flutter_graphql 1.0.0-rc.3 copy "flutter_graphql: ^1.0.0-rc.3" to clipboard
flutter_graphql: ^1.0.0-rc.3 copied to clipboard

discontinued

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

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 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.1
copied to clipboard

Now inside your Dart code you can import it.

import 'package:flutter_graphql/flutter_graphql.dart';
copied to clipboard

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,
    ),
  );

  ...
}

...
copied to clipboard

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',
      ...
    ),
  );

  ...
copied to clipboard

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',
          ...
        ),
      ),
    );
  }
}

...
copied to clipboard

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,
    ),
  );
    
copied to clipboard

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,
  ),
);
copied to clipboard

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;
}
copied to clipboard

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
    }
  }
''';
copied to clipboard

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']);
    });
  },
);

...
copied to clipboard

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,
  ),
);

''';
copied to clipboard

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']);
  },
);

...
copied to clipboard

Mutations #

Again first create a mutation string:

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

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),
    );
  },
);

...
copied to clipboard

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');
copied to clipboard

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');
            }
          }
        ),
      )
    );
  }
}
copied to clipboard

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'),
      );
    },
  );

  ...
copied to clipboard

A different client:

  ...

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

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

  ...
copied to clipboard

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]
  }
  ''';

  ...
copied to clipboard

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

  ...

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

  ...
copied to clipboard

or

  ...

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

  ...
copied to clipboard

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;
    }
  }

  ...
copied to clipboard

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;
  }
}
copied to clipboard

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!

1
likes
35
points
33
downloads

Publisher

unverified uploader

Weekly Downloads

2024.09.22 - 2025.04.06

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

Repository (GitHub)

License

MIT (license)

Dependencies

flutter, graphql_parser, http, http_parser, meta, path_provider, sqflite, uuid

More

Packages that depend on flutter_graphql