Skip to content
Hironobu Iga

Persisting the cache in Flutter's GraphQLClient

How to persist a GraphQL client's cache with HiveStore, and how to avoid the mess on the Riverpod side that comes from its initialisation being asynchronous.

Published

This article is also published elsewhere. https://iganin.hatenablog.com/entry/2021/09/03/102654

Originally written in Japanese. This is a translation of the same piece.

TL;DR

  • Use HiveStore
  • Initialising HiveStore is asynchronous, so the graphql provider ends up as a Future, which is a pain
  • To get around that, finish the initialisation in main.dart and override the provider in ProviderScope

Environment

[✓] Flutter (Channel stable, 2.2.3, on macOS 11.3.1 20E241 darwin-x64, locale ja-JP)
[✓] Xcode - develop for iOS and macOS
[✓] Chrome - develop for the web
[✓] Android Studio (version 2020.3)
[✓] IntelliJ IDEA Ultimate Edition (version 2021.1.2)
[✓] VS Code (version 1.59.1)

hooks_riverpod: 0.14.0+4
graphql: 5.0.0

Cache persistence

GraphqlClient manages the cache for you inside the client. (Set it up right and it even supports normalisation — excellent.) The default is an in-memory cache, but you can also persist it to a database, which strikes me as ideal if you want a transparent cache.

To persist the cache in GraphqlClient, use HiveStore. With GraphqlFlutter the initialisation is just a call to initHiveForFlutter() in main.dart, but with Graphql it takes a bit more work.

Here is one way to do it. Get the path where the database should live, then open the HiveStore. By default the box it holds internally is named graphqlClientStore, but open takes a name argument, so you can change it. Pass GraphQLCache(store: hiveStore) as the cache when you create the GraphQLClient and you are done.

final appDir = await getApplicationDocumentsDirectory();
final path = appDir.path;
final store = await HiveStore.open(path: path);

final client = GraphQLClient(
        link: authLink.concat(_baseLink), cache: GraphQLCache(store: store));

When you are injecting with Riverpod

If you are using Riverpod for DI, you want to get graphqlClient through a provider too. But initialising hiveStore is asynchronous, so the obvious approach leaves you with a FutureProvider, which is a real nuisance on the calling side. It is the same problem you hit with SharedPreferences.

So: create a provider whose job is to hand over the hiveStore, do the initialisation in main.dart, and override the provider inside ProviderScope. That absorbs the asynchronous part.

final hiveStoreProvider = Provider<HiveStore>((ref) {
  throw Exception('Provider was not initialized');
});

Future<void> main() async {
~~~~
  var appDir = await getApplicationDocumentsDirectory();
  var path = appDir.path;
  final store = await HiveStore.open(path: path);

  runApp(
    ProviderScope(
      overrides: [
        hiveStoreProvider.overrideWithValue(store),
      ],
      child: App(),
    ),
  );

~~~~
}

With that in place, the graphqlClient provider can be called synchronously:

final gqlClientProvider = Provider((ref) => AppGqlClient((ref.read)));

Closing

GraphqlClient is handy.