Skip to content

Latest commit

 

History

History
518 lines (413 loc) · 19.5 KB

README.md

File metadata and controls

518 lines (413 loc) · 19.5 KB

GraphQL Flutter

Build Status Coverage 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 usage. 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 performance client.

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

Installation

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

dependencies:
  graphql_flutter: ^1.0.0-beta

Now inside your Dart code you can import it.

import 'package:graphql_flutter/graphql_flutter.dart';

Upgrading from 0.x.x

Here is a guide to fix most of the breaking changes introduced in 1.x.x.

Some class names have been renamed:

  • Renamed Client to GraphQLClient
  • Renamed GraphqlProvider to GraphQLProvider
  • Renamed GraphqlConsumer to GraphQLConsumer
  • Renamed GQLError to GraphQLError

We changed the way the client handles requests, it now uses a Link to execute queries rather then depend on the http package. We've currently only implemented the HttpLink, just drop it in like so:

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

-  ValueNotifier<Client> client = ValueNotifier(
+  ValueNotifier<GraphQLClient> client = ValueNotifier(
-  Client(
-    endPoint: 'https://api.github.com/graphql',
+  GraphQLClient(
      cache: InMemoryCache(),
-      apiToken: '<YOUR_GITHUB_PERSONAL_ACCESS_TOKEN>',
+      link: link,
    ),
  );
}

We have made a load of changes how queries and mutations work under the hood. To allow for these changes we had to make some small changes to the API of the Query and Mutation widgets.

Query(
-  readRepositories,
+  options: QueryOptions(
+    document: readRepositories,
    variables: {
      'nRepositories': 50,
    },
    pollInterval: 10,
+  ),
-  builder: ({
-    bool loading,
-    var data,
-    String error,
-  }) {
+  builder: (QueryResult result, { VoidCallback refetch }) {
-    if (error != '') {
-      return Text(error);
+    if (result.errors != null) {
+      return Text(result.errors.toString());
    }

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

-    List repositories = data['viewer']['repositories']['nodes'];
+    List repositories = result.data['viewer']['repositories']['nodes'];

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

        return Text(repository['name']);
    });
  },
);
Mutation(
-  addStar,
+  options: MutationOptions(
+    document: addStar,
+  ),
  builder: (
-    runMutation, {
-    bool loading,
-    var data,
-    String error,
+    RunMutation runMutation,
+    QueryResult result,
-  }) {
+  ) {
    return FloatingActionButton(
      onPressed: () => runMutation({
        'starrableId': <A_STARTABLE_REPOSITORY_ID>,
      }),
      tooltip: 'Star',
      child: Icon(Icons.star),
    );
  },
);

That's it! You should now be able to use the latest version of our library.

Usage

To use the client it first needs to be initialized with an link and cache. For this example we will be using an HttpLink as our link and InMemoryCache as our cache. If your endpoint requires authentication you can concatenate the AuthLink, it resolves the credentials using a future, so you can authenticate asynchronously.

For this example we will use the public GitHub API.

...

import 'package:graphql_flutter/graphql_flutter.dart';

void main() {
  final HttpLink httpLink = HttpLink(
    uri: 'https://api.github.com/graphql',
  );

  final AuthLink authLink = AuthLink(
    getToken: () => async 'Bearer <YOUR_PERSONAL_ACCESS_TOKEN>',
  );

  final Link link = authLink.concat(httpLink);

  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 through the build context.

...

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

...

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 graphql-flutter 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

Creating a query is as simple as creating a multiline string:

String readRepositories = """
  query ReadRepositories(\$nRepositories) {
    viewer {
      repositories(last: \$nRepositories) {
        nodes {
          id
          name
          viewerHasStarred
        }
      }
    }
  }
"""
    .replaceAll('\n', ' ');

In your widget:

...

Query(
  options: QueryOptions(
    document: readRepositories, // this is the query string you just created
    variables: {
      'nRepositories': 50,
    },
    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 repositories = result.data['viewer']['repositories']['nodes'];

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

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

...

Mutations

Again first create a mutation string:

String addStar = """
  mutation AddStar(\$starrableId: ID!) {
    addStar(input: {starrableId: \$starrableId}) {
      starrable {
        viewerHasStarred
      }
    }
  }
"""
    .replaceAll('\n', ' ');

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: addStar, // this is the mutation string you just created
  ),
  builder: (
    RunMutation runMutation,
    QueryResult result,
  ) {
    return FloatingActionButton(
      onPressed: () => runMutation({
        'starrableId': <A_STARTABLE_REPOSITORY_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 instance of socketClient needs to be initialized.

We are working on moving this into the same GraphQLProvider structure 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 directly from the GraphQLProvider but to make it even easier you can also use the GraphQLConsumer widget.

  ...

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

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

  ...

Graphql Upload

We support GraphQL Upload spec as proposed at https://github.com/jaydenseric/graphql-multipart-request-spec

mutation($files: [Upload!]!) {
  multipleUpload(files: $files) {
    id
    filename
    mimetype
    path
  }
}
import 'dart:io' show File;

// ...

String filePath = '/aboslute/path/to/file.ext';
final QueryResult r = await graphQLClientClient.mutate(
  MutationOptions(
    document: uploadMutation,
    variables: {
      'files': [File(filePath)],
    },
  )
);

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
GraphQL pload
Optimistic results 🔜
Client state management 🔜
Modularity 🔜

Contributing

Feel free to open a PR with any suggestions! We'll be actively working on the library ourselves.

Contributors

This package was originally created and published by the engineers at Zino App BV. Since then the community has helped to make it even more useful for even more developers.

Thanks goes to these wonderful people (emoji key):


Eustatiu Dima

🐛 💻 📖 💡 🤔 👀

Zino Hofmann

🐛 💻 📖 💡 🤔 🚇 👀

Harkirat Saluja

📖 🤔

Chris Muthig

💻 📖 💡 🤔

Cal Pratt

🐛 💻 📖 💡 🤔

Miroslav Valkovic-Madjer

💻

Aleksandar Faraj

🐛

Arnaud Delcasse

🐛 💻

Dustin Graham

🐛 💻

Fábio Carneiro

🐛

Gregor

🐛 💻 🤔

Kolja Esders

🐛 💻 🤔

Michael Joseph Rosenthal

🐛 💻 📖 💡 🤔 ⚠️

Igor Borges

🐛 💻

Rafael Ring

🐛 💻

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