ferry 0.3.1+1 copy "ferry: ^0.3.1+1" to clipboard
ferry: ^0.3.1+1 copied to clipboard

outdated

GraphQL Client for Dart

Stream Based GraphQL Client for Dart

MIT License PRs Welcome Star on GitHub Watch on GitHub Pub Version

Warning: This library is still a work in progress. The API may change.

Features #

Feature Progress
Generated Fully Typed Queries and Resposnes (using gql_build)
Customizable Links (using gql_link)
Optimistic Cache
Multiple data stores, including MemoryStore and HiveStore (using hive for offline persistence)
Update queries with additinal data (e.g. for pagination)
Flutter Widget
Offline Mutations 🔜

Architecture #

  1. Code Builders (from gql_build):
    1. Create dart representations of all queries (including their variables, inputs, and data)
    2. Using the additional req_builder included with this package, generate typed QueryRequest objects which allow the client to parse typed responses.
  2. Client:
    1. Handles configuration
    2. Routes QueryRequests to the cache or network, based on the given FetchPolicy
    3. Generates streams of QueryResponses for a given QueryRequest
  3. Link (from gql_link): Handles GraphQL network requests
  4. Cache:
    1. Normalizes and denormalizes data for queries and fragments (using the normalize package)
    2. Maintains a collection of Optimistic Patches and handles optimistic reads and writes
  5. Store: Persists data

Usage #

Setup Client #

Add ferry and gql_http_link to your pubspec.

Simple #

import 'package:gql_http_link/gql_http_link.dart';
import 'package:ferry/ferry.dart';

final link = HttpLink("[path/to/endpoint]");

final client = Client(link: link);

This instantiates a client with the default configuration, including a Cache instance that uses a MemoryStore to store data.

With HiveStore (persisted offline data) #

Add hive (and hive_flutter if you're using flutter) to your pubspec.

import 'package:gql_http_link/gql_http_link.dart';
import 'package:ferry/ferry.dart';
import 'package:hive/hive.dart';
// *** If using flutter ***
// import 'package:hive_flutter/hive_flutter.dart';

Future<Client> initClient() async {
  Hive.init();
  // OR, if using flutter
  // await Hive.initFlutter();

  final box = await Hive.openBox("graphql");

  final store = HiveStore(box);

  final cache = Cache(dataStore: store);

  final link = HttpLink("[path/to/endpoint]");

  final client = Client(
    link: link,
    cache: cache,
  );

  return client;
}

With UpdateCacheHandlers #

The Client allows arbitrary cache updates following mutations, similar to functionality provided by Apollo Client's mutation update function. However, in order for mutations to work offline (still a WIP), the client must be aware of all UpdateCacheHandlers.

typedef UpdateCacheHandler<T> = void Function(
  CacheProxy proxy,
  QueryResponse<T> response,
);

CacheProxy provides methods to readQuery, readFragment, writeQuery, and writeFragment.

import 'package:gql_http_link/gql_http_link.dart';
import 'package:ferry/ferry.dart';

import '[path/to/MyUpdateCacheHandler]';

final link = HttpLink("https://graphql-pokemon.now.sh/graphql");

final updateCacheHandlers = <dynamic, Function>{
  "MyHandlerKey": MyUpdateCacheHandler,
};

final options = ClientOptions(updateCacheHandlers: updateCacheHandlers);

final client = Client(
  link: link,
  options: options,
);

This handler can then be called using its key "MyHandlerKey" from a QueryRequest.

Generate Dart GraphQL Files #

The Client is fully typed, so we must use the gql_build package to generate dart representations of our GraphQL queries. We will also use the req_builder included in the Client package to build typed QueryRequests for each GraphQL query.

Download GraphQL Schema #

First, we need to downoad our GraphQL in SDL format to any location within the lib project directory. You can use the get-graphql-schema tool to download a schema from a GraphQL endpoint:

First, install the tool:

npm install -g get-graphql-schema

Next, download the schema:

get-graphql-schema ENDPOINT_URL > lib/schema.graphql

Add Queries to .graphql files #

gql_build will generate dart code for all files located in the lib folder that end in a .graphql extention.

For example, we might have the following in all_pokemon.graphql:

query AllPokemon($first: Int!) {
  pokemons(first: $first) {
    id
    name
    maxHP
    image
  }
}

Build Generated Queries #

Add gql_build and build_runner to your dev_dependencies in your pubspec file.

Next add a build.yaml file to your project root:

targets:
  $default:
    builders:
      gql_build|schema_builder:
        enabled: true
      gql_build|ast_builder:
        enabled: true
      gql_build|op_builder:
        enabled: true
        options:
          schema: your_package_name|lib/schema.graphql
      gql_build|data_builder:
        enabled: true
        options:
          schema: your_package_name|lib/schema.graphql
      gql_build|var_builder:
        enabled: true
        options:
          schema: your_package_name|lib/schema.graphql

      ferry|req_builder:
        enabled: true

Now we can build our dart generated files by calling:

pub run build_runner build

Or, if we are using flutter

flutter pub run build_runner build

Queries #

import 'path/to/client.dart';
import './[my_query].req.gql.dart';

// Instantiate a `QueryRequest` using the generated `.req.gql.dart` file.
final query = MyQuery(buildVars: (b) => b..id = "123");

// Listen to responses for the given query
client.responseStream(query).listen((response) => print(response));

Mutations #

Mutations are executed in the same way as queries

import 'path/to/client.dart';
import './[my_mutation].req.gql.dart';

// Instantiate a `QueryRequest` using the generated `.req.gql.dart` file.
final mutation = MyMutation(buildVars: (b) => b..id = "123");

// If I only care about the first non-optimistic response, I can do:
client
  .responseStream(mutation)
  .firstWhere((response) => response.source != ResponseSource.Optimistic)
  .then((response) => print(response));

With Flutter #

The library includes a Query flutter widget, which is a simple wrapper around the StreamBuilder widget.

This example assumes we've registered our Client instance with get_it, but you can use any dependency injection.

import 'package:flutter/material.dart';
import 'package:ferry/ferry.dart';
import 'package:get_it/get_it.dart';

import './my_query.data.gql.dart';
import './my_query.req.gql.dart';

class AllPokemonScreen extends StatelessWidget {
  final client = GetIt.I<Client>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('All Pokemon'),
      ),
      body: Query(
        client: client,
        queryRequest: AllPokemon(
          buildVars: (vars) => vars..first = 500,
        ),
        builder: (
          BuildContext context,
          QueryResponse<$AllPokemon> response,
        ) {
          if (response.loading)
            return Center(child: CircularProgressIndicator());

          final pokemons = response.data?.pokemons ?? [];

          return ListView.builder(
            itemCount: pokemons.length,
            itemBuilder: (context, index) => PokemonCard(
              pokemon: pokemons[index],
            ),
          );
        },
      ),
    );
  }
}