GithubHelp home page GithubHelp logo

snowballdigital / flutter-graphql Goto Github PK

View Code? Open in Web Editor NEW
47.0 7.0 7.0 618 KB

A GraphQL client for Flutter, bringing all the features from a modern GraphQL client to one easy to use package. Built after react apollo

License: MIT License

Java 0.50% Ruby 2.91% Objective-C 1.03% Dart 95.57%
react-apollo-graphql flutter graphql graphql-client flutter-library

flutter-graphql's Introduction

Flutter GraphQL

version MIT License All Contributors PRs Welcome

Watch on GitHub Star on GitHub

Table of Contents

About this project

GraphQL brings many benefits, both to the client: devices will need less requests, and therefore reduce data useage. And to the programer: requests are arguable, they have the same structure as the request.

This project combines the benefits of GraphQL with the benefits of Streams in Dart to deliver a high performace client.

The project took inspriation from the Apollo GraphQL client, great work guys!

Note: Still in Beta Docs is coming soon Support for all React Apollo Graphql component supported props is coming soon

Installation

First depend on the library by adding this to your packages pubspec.yaml:

dependencies:
  flutter_graphql: ^1.0.0-rc.3

Now inside your Dart code you can import it.

import 'package:flutter_graphql/flutter_graphql.dart';

Usage

To use the client it first needs to be initialized with an link and cache. For this example we will be uing an HttpLink as our link and InMemoryCache as our cache. If your endpoint requires authentication you can provide some custom headers to HttpLink.

For this example we will use the public GitHub API.

...

import 'package:flutter_graphql/flutter_graphql.dart';

void main() {
  HttpLink link = HttpLink(
    uri: 'https://api.github.com/graphql',
    headers: <String, String>{
      'Authorization': 'Bearer <YOUR_PERSONAL_ACCESS_TOKEN>',
    },
  );

  ValueNotifier<GraphQLClient> client = ValueNotifier(
    GraphQLClient(
      cache: InMemoryCache(),
      link: link,
    ),
  );

  ...
}

...

GraphQL Provider

In order to use the client, you Query and Mutation widgets to be wrapped with the GraphQLProvider widget.

We recommend wrapping your MaterialApp with the GraphQLProvider widget.

  ...

  return GraphQLProvider(
    client: client,
    child: MaterialApp(
      title: 'Flutter Demo',
      ...
    ),
  );

  ...

Offline Cache

The in-memory cache can automatically be saved to and restored from offline storage. Setting it up is as easy as wrapping your app with the CacheProvider widget.

It is required to place the CacheProvider widget is inside the GraphQLProvider widget, because GraphQLProvider makes client available trough the build context.

...

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GraphQLProvider(
      client: client,
      child: CacheProvider(
        child: MaterialApp(
          title: 'Flutter Demo',
          ...
        ),
      ),
    );
  }
}

...

Graphql Link and Headers

You can setup authentication headers and other custom links just like you do with Apollo Graphql

  import 'dart:async';

  import 'package:flutter/material.dart';
  import 'package:flutter_graphql/flutter_graphql.dart';
  import 'package:flutter_graphql/src/link/operation.dart';
  import 'package:flutter_graphql/src/link/fetch_result.dart';

  class AuthLink extends Link {
    AuthLink()
        : super(
      request: (Operation operation, [NextLink forward]) {
        StreamController<FetchResult> controller;

        Future<void> onListen() async {
          try {
            var token = await AuthUtil.getToken();
            operation.setContext(<String, Map<String, String>>{
              'headers': <String, String>{'Authorization': '''bearer $token'''}
            });
          } catch (error) {
            controller.addError(error);
          }

          await controller.addStream(forward(operation));
          await controller.close();
        }

        controller = StreamController<FetchResult>(onListen: onListen);

        return controller.stream;
      },
    );
  }

  var cache = InMemoryCache();

  var authLink = AuthLink()
      .concat(HttpLink(uri: 'http://yourgraphqlserver.com/graphql'));
      
  final ValueNotifier<GraphQLClient> client = ValueNotifier(
    GraphQLClient(
      cache: cache,
      link: authLink,
    ),
  );

  final ValueNotifier<GraphQLClient> anotherClient = ValueNotifier(
    GraphQLClient(
      cache: cache,
      link: authLink,
    ),
  );
    

However note that flutter-graphql does not inject __typename into operations the way apollo does, so if you aren't careful to request them in your query, this normalization scheme is not possible.

Normalization

To enable apollo-like normalization, use a NormalizedInMemoryCache:

ValueNotifier<GraphQLClient> client = ValueNotifier(
  GraphQLClient(
    cache: NormalizedInMemoryCache(
      dataIdFromObject: typenameDataIdFromObject,
    ),
    link: link,
  ),
);

dataIdFromObject is required and has no defaults. Our implementation is similar to apollo's, requiring a function to return a universally unique string or null. The predefined typenameDataIdFromObject we provide is similar to apollo's default:

String typenameDataIdFromObject(Object object) {
  if (object is Map<String, Object> &&
      object.containsKey('__typename') &&
      object.containsKey('id')) {
    return "${object['__typename']}/${object['id']}";
  }
  return null;
}

However note that flutter-graphql does not inject __typename into operations the way apollo does, so if you aren't careful to request them in your query, this normalization scheme is not possible.

Queries

To create a query, you just need to define a String variable like the one below. With full support of fragments

const GET_ALL_PEOPLE = '''
  query getPeople{
    readAll{
      name
      age
      sex
    }
  }
''';

In your widget:

...

Query(
  options: QueryOptions(
    document: GET_ALL_PEOPLE, // this is the query string you just created
    pollInterval: 10,
  ),
  builder: (QueryResult result) {
    if (result.errors != null) {
      return Text(result.errors.toString());
    }

    if (result.loading) {
      return Text('Loading');
    }

    // it can be either Map or List
    List people = result.data['getPeople'];

    return ListView.builder(
      itemCount: people.length,
      itemBuilder: (context, index) {
        final repository = people[index];

        return Text(people['name']);
    });
  },
);

...

Other examples with query argments and passing in a custom graphql client

const READ_BY_ID = '''
  query readById(\$id: String!){
    readById(ID: \$id){
      name
      age
      sex
    }
  }
  
  
final ValueNotifier<GraphQLClient> userClient = ValueNotifier(
  GraphQLClient(
    cache: cache,
    link: authLinkProfile,
  ),
);

''';

In your widget:

...

Query(
  options: QueryOptions(
    document: READ_BY_ID, // this is the query string you just created
    pollInterval: 10,
    client: userClient.value
  ),
  builder: (QueryResult result) {
    if (result.errors != null) {
      return Text(result.errors.toString());
    }

    if (result.loading) {
      return Text('Loading');
    }

    // it can be either Map or List
    List person = result.data['getPeople'];

    return Text(person['name']);
  },
);

...

Mutations

Again first create a mutation string:

const LIKE_BLOG = '''
  mutation likeBlog(\$id: Int!) {
    likeBlog(id: \$id){
      name
      author {
        name
        displayImage
      }
  }
''';

The syntax for mutations is fairly similar to that of a query. The only diffence is that the first argument of the builder function is a mutation function. Just call it to trigger the mutations (Yeah we deliberately stole this from react-apollo.)

...

Mutation(
  options: MutationOptions(
    document: LIKE_BLOG, // this is the mutation string you just created
  ),
  builder: (
    RunMutation runMutation,
    QueryResult result,
  ) {
    return FloatingActionButton(
      onPressed: () => runMutation({
        'id': <BLOG_ID>,
      }),
      tooltip: 'Star',
      child: Icon(Icons.star),
    );
  },
);

...

Subscriptions (Experimental)

The syntax for subscriptions is again similar to a query, however, this utilizes WebSockets and dart Streams to provide real-time updates from a server. Before subscriptions can be performed a global intance of socketClient needs to be initialized.

We are working on moving this into the same GraphQLProvider stucture as the http client. Therefore this api might change in the near future.

socketClient = await SocketClient.connect('ws://coolserver.com/graphql');

Once the socketClient has been initialized it can be used by the Subscription Widget

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Subscription(
          operationName,
          query,
          variables: variables,
          builder: ({
            bool loading,
            dynamic payload,
            dynamic error,
          }) {
            if (payload != null) {
              return Text(payload['requestSubscription']['requestData']);
            } else {
              return Text('Data not found');
            }
          }
        ),
      )
    );
  }
}

Graphql Consumer

You can always access the client direcly from the GraphQLProvider but to make it even easier you can also use the GraphQLConsumer widget. You can also pass in a another client to the consumer

  ...

  return GraphQLConsumer(
    builder: (GraphQLClient client) {
      // do something with the client

      return Container(
        child: Text('Hello world'),
      );
    },
  );

  ...

A different client:

  ...

  return GraphQLConsumer(
    client: userClient,
    builder: (GraphQLClient client) {
      // do something with the client

      return Container(
        child: Text('Hello world'),
      );
    },
  );

  ...

Fragments

There is support for fragments and it's basically how you use it in Apollo React. For example define your fragment as a dart String.

  ...
const UserFragment = '''
  fragment UserFragmentFull on Profile {
    address {
      city
      country
      postalCode
      street
    }
    birthdate
    email
    firstname
    id
    lastname
  }
  ''';

  ...

Now you can use it in your Graphql Query or Mutation String like below

  ...

  const CURRENT_USER = '''
    query read{
      read {
      ...UserFragmentFull
      }
    }
    $UserFragment
  ''';

  ...

or

  ...

  const GET_BLOGS = '''
    query getBlogs{
      getBlog {
        title
        description
        tags
        
        author {
          ...UserFragmentFull
        }
    }
    $UserFragment
  ''';

  ...

Outside a Widget

Similar to withApollo or graphql HoC that passes the client to the component in react, you can call a graphql query from any part of your code base even in a your service class or in your Scoped MOdel or Bloc class. Example

  ...

  class AuthUtil{
    static Future<String> getToken() async {
      SharedPreferences prefs = await SharedPreferences.getInstance();
      return await prefs.getString('token');
    }

    static Future setToken(value) async {
      SharedPreferences prefs = await SharedPreferences.getInstance();
      return await prefs.setString('token', value);
    }

    static removeToken() async {
      SharedPreferences prefs = await SharedPreferences.getInstance();
      return await prefs.remove('token');
    }

    static clear() async {
      SharedPreferences prefs = await SharedPreferences.getInstance();
      return await prefs.clear();
    }
    
    static Future<bool> logIn(String username, String password) async {
      var token;

      QueryOptions queryOptions = QueryOptions(
          document: LOGIN,
          variables: {
            'username': username,
            'password': password
          }
      );

      if (result != null) {
        this.setToken(result);
        return clientProfile.value.query(queryOptions).then((result) async {

          if(result.data != null) {
            token = result.data['login']['token];
            notifyListeners();
            return token;
          } else {
            return throw Error;
          }

        }).catchError((error) {
            return throw Error;
        });
      } else
        return false;
    }
  }

  ...

In a scoped model:

  ...
class AppModel extends Model {

  String token = '';
  var currentUser = new Map <String, dynamic>();

  static AppModel of(BuildContext context) =>
      ScopedModel.of<AppModel>(context);

  void setToken(String value) {
    token = value;
    AuthUtil.setAppURI(value);
    notifyListeners();
  }


  String getToken() {
    if (token != null) return token;
    else AuthUtil.getToken();
  }

  getCurrentUser() {
    return currentUser;
  }

  Future<bool> isLoggedIn() async {

    var result = await AuthUtil.getToken();
    print(result);

    QueryOptions queryOptions = QueryOptions(
        document: CURRENT_USER
    );

    if (result != null) {
      print(result);
      this.setToken(result);
      return clientProfile.value.query(queryOptions).then((result) async {

        if(result.data != null) {
          currentUser = result.data['read'];
          notifyListeners();
          return true;
        } else {
          return false;
        }

      }).catchError((error) {
        print('''Error => $error''');
        return false;
      });
    } else
      return false;
  }
}

Roadmap

This is currently our roadmap, please feel free to request additions/changes.

Feature Progress
Queries
Mutations
Subscriptions
Query polling
In memory cache
Offline cache sync
Optimistic results 🔜
Client state management 🔜
Modularity 🔜
Documentation 🔜

Contributing

Feel free to open a PR with any suggestions! We'll be actively working on the library ourselves. If you need control to the repo, please contact me Rex Raphael. Please fork and send your PRs to the v.1.0.0 branch.

This project follows the all-contributors specification. Contributions of any kind are welcome!

flutter-graphql's People

Contributors

adelcasse avatar aleksandarfaraj avatar cal-pratt avatar camuthig avatar comigor avatar dustin-graham avatar eusdima avatar fabiocarneiro avatar hofmannz avatar ianko avatar juicycleff avatar kolja-esders avatar leoafarias avatar lordgreg avatar micimize avatar mmadjer avatar rafaelring avatar salujaharkirat avatar sirkuryaki avatar smidds avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

flutter-graphql's Issues

subscription foreground

Is your feature request related to a problem? Please describe.
now I can't send a request in the background, as well as send and subscribe

Describe the solution you'd like
i don't no :(

Describe alternatives you've considered
i propabbly use this package https://pub.dev/packages/workmanager

Additional context
Add any other context or screenshots about the feature request here.

Question: Purpose of "getOperationName" function

While looking through the source of this package, I found this comment:
https://github.com/snowballdigital/flutter-graphql/blob/master/lib/src/core/query_manager.dart#L67-L68:

// XXX there is a bug in the `graphql_parser` package, where this result might be
// null event though the operation name is present in the document

I am the creator/maintainer of the graphql_parser package. Can you provide some details about the bug you are referring to?

In a query like this:

{
  foo,
  bar: baz
  ...quux
  ... on A {b, c}
}

There is no operation name, and this a pretty common case.

Is it possible that the query you were using simply didn't have an operation name? Otherwise, if you can provide a repro, I can fix it.

Relies on an older version of path_provider

Would it be possible to update the path_provider dependency to the latest version, i am getting incompatible since it requires version ^0.4.1 and using Cached Network Image which is on version ^0.5.0+1.

Because cached_network_image >=0.7.0 depends on flutter_cache_manager ^0.3.2 which depends on path_provider ^0.5.0+1, cached_network_image >=0.7.0 requires path_provider ^0.5.0+1.

And because flutter_graphql 1.0.0-rc.3 depends on path_provider ^0.4.1 and no versions of flutter_graphql match >1.0.0-rc.3 <2.0.0, cached_network_image >=0.7.0 is incompatible with flutter_graphql ^1.0.0-rc.3.

WANT TO BE A CORE CONTIBUTOR

Do you want to be a core contributor, please drop your names so we can discuss the way forward for the library. We need a very stable GraphQL library for Flutter.

Loading boolean only set true on initial load

Describe the bug
The loading boolean is only ever true, once. Meaning, if you were to wrap the Query in a StatefulWidget, or a ScopedModelDescendant as in my case, the build method causes the Query to fire off its builder, but the result.loading is never actually set to true to indicate that the Query is loading again.

To Reproduce
Steps to reproduce the behavior:

  1. Construct a StatefulWidget or ScopedModelDescedant, or anything that will fire off a rebuild
  2. Set a Query widget as a child of your StatefulWidget
  3. Create an if statement within the builder for Query to print out "Loading!" when result.loading is true
  4. Add a button or some other trigger that will change the state of the Queries parent, firing the parent's build method
  5. Load the application
  6. Notice that "Loading!" fires on first application load
  7. Press the button / fire the trigger
  8. Notice that "Loading!" does not appear in the log, indicating that result.loading is never reset to true

Expected behavior
This might be my misunderstanding when it comes to the loading boolean, but I anticipated that loading would be set to true whenever the Query is fetching data, and especially when the whole widget is rebuilding.

Screenshots
n/a

Desktop (please complete the following information):

  • OS: Windows
  • Version 10

Smartphone (please complete the following information):

  • Device: Genymotion Emulator
  • OS: Android Pie
  • Version 1.0.0-rc.3

Additional context
I've done some digging around in the query.dart file and I've found that everything appears to initialize as expected when the dependencies change, and the Query widget's internal build method does in fact fire every time we run its StatefulWidget parent's build method, which does in fact set initialData: QueryResult { loading: true } (line 88). My best guess is that this has something to do with not fully closing the stream, or perhaps something to do with the controller? No idea!

Re-sending a failed query

I've integrated the Query widget into my app, and it works just fine the first time it runs. However, I'd like to have a "try again" button if the query fails that would re-send the query. However, I can't figure out how to refresh/re-send a query. I've written the following code so far, and every time I hit "try again", I can see Built event widget tree printed to my console, but the query itself isn't re-sent:

@override Widget build(BuildContext context) {
  print("Built event widget tree");
  const request = '''
  {
    festapp(id:"931") {
      events {
        name
        startsAt
        image
      }
    }
  }
  ''';
  return Query(
      options: QueryOptions(
        document: request,
        fetchPolicy: FetchPolicy.noCache,
        errorPolicy: ErrorPolicy.all,
        pollInterval: 5
      ),
      builder: (QueryResult result) {
        if (result.errors != null) {
          return Center(child:
            Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Text(result.errors.toString()),
                CupertinoButton.filled(
                  child: Text("Try again"),
                  onPressed: (){
                    setState((){});
                  },
                )
              ],
            )
          );
        }

        if (result.loading) {
          return Center(
            child: CupertinoActivityIndicator(
              radius: 20,
              animating: true,
            ),
          );
        }

        List events = result.data["festapp"]["events"];

        return ListView.separated(itemBuilder: (context, row) => EventCell(Event.fromAPIData(events[row])),
            separatorBuilder: (context, row) {
              return Container(
                height: 1,
                color: Color.fromARGB(20, 0, 0, 0),
                margin: EdgeInsets.only(left: 8),
              );
            },
            itemCount: events.length);
        }
    );
  }

pollInterval should not poll the server when set to zero

Describe the bug
When pollInterval is set to zero, it constantly polls to the server, without stopping. Since it follows Apollo GrapphQL patterns, this should disable polling altogether.

To Reproduce

Set pollInterval to 0 at QueryOptions

Expected behavior
Should disable polling altogether

Comparison with https://github.com/zino-app/graphql-flutter

Hi, this package was originally a fork of https://github.com/zino-app/graphql-flutter: https://github.com/juicycleff/flutter-graphql
Then it transferred to another namespace (snowballdigital) and it lost its reference as a fork of zino-app.

Could you please describe how this package differs from the original package that is still being maintained and what design decisions made you decide to go a different direction (and not try to steer to a single effort, maintained and contributed by more devs?)

We as a team are also willing to contribute, but must decide first what package is the most promising to end up as the project with most traction and community support.

Thanks!

Problem with subscription

Hello, I have a problem with Graphql subscription. When I try with graphql playground - all works fine: on the server I receive an answer about subscription, but when I trying to connect in flutter - I'm getting only loading true on the client, but nothing on the server.

import 'package:flutter/material.dart';
import 'package:flutter_graphql/flutter_graphql.dart';

void main() async {
  socketClient = await SocketClient.connect('ws://address',
    headers: {
      'authorization': "accessTokenHere"
    }
  );

  runApp(MyApp());
}

class MyApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    final title = 'WebSocket Demo';
    return MaterialApp( 
      title: title,
      home: MyHomePage(
        title: title,
      ),
    );
  }
}

class MyHomePage extends StatefulWidget {
  final String title;

  MyHomePage({Key key, @required this.title})
      : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  TextEditingController _controller = TextEditingController();

  static String operationName = "notification";
  String query = """subscription $operationName{
    notification{
      messageCount
    }
  }""".replaceAll('\n', ' ');

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Padding(
        padding: const EdgeInsets.all(20.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Form(
              child: TextFormField(
                controller: _controller,
                decoration: InputDecoration(labelText: 'Send a message'),
              ),
            ),
            Subscription(
              operationName,
              query,
              variables: {},
              builder: ({
                bool loading,
                dynamic payload,
                dynamic error,
              }) {
                print(loading);
                print(payload);
                print(error);
                if (payload != null) {
                  return Text(payload['requestSubscription']['requestData']);
                } else {
                  return Text('Data not found');
                }
              }
            ),
          ],
        ),
      ),
    );
  }

}

why not transfer ownership?

@juicycleff you can transfer repository ownership, which I think would be less confusing than copy-pasting for end-users, and maintain the github repo-relationship graph
Screen Shot 2019-03-19 at 18 52 25

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.