Jaspr Riverpod

A port of the riverpod package for Jaspr.

Jaspr comes with a Riverpod package that ports over flutter_riverpod to Jaspr. It is based on Riverpod 2 and supports all providers and modifiers.

Accessing Providers

While it has feature-parity for defining Providers, it comes with some quality-of-life improvements on the Consumer side. Mainly:

It does not have Consumer, ConsumerWidget or StatefulConsumerWidget. This is because it does not rely on WidgetRef to access providers but instead comes with context extensions on the BuildContext of any component.

As an example, this (in Flutter):

Flutter
// need to extend custom ConsumerWidget
class MyWidget extends ConsumerWidget {
  // need to accept custom WidgetRef
  Widget build(BuildContext context, WidgetRef ref) {
    // uses ref to access providers
    var value = ref.watch(myProvider);
    return Text(value);
  }
}

is equivalent to this (in Jaspr)

Jaspr
// just extends the normal component
class MyComponent extends StatelessComponent {
  // no extra parameter
  Component build(BuildContext context) {
    // uses context to access providers
    var value = context.watch(myProvider);
    return Text(value);
  }
}

The extension on BuildContext supports all the normal methods from `WidgetRef``

  • context.read(),
  • context.watch(),
  • context.refresh(),
  • context.invalidate(),
  • context.listen()

Replacement for Consumer

Same as with ConsumerComponent, we don't need the Consumer anymore. If you want only parts of your component tree to rebuild when watching a provider, simply use the Builder component. This will give you a new context which you can call context.watch on:

Builder(builder: (context) {
  var value = context.watch(...);
  return /* ... */;
});

Syncing Provider State

Jaspr allows data that is loaded during pre-rendering (in server or static mode) to be synced to the client for further use. jaspr_riverpod extends this for providers, allowing to sync provider state from server to client like this:

@override
Component build() {
  return ProviderScope(
    sync: [
      myProvider.syncWith('some-unique-key'),
    ],
    child: ...
  );
}

This will cause the value of myProvider to be evaulated and serialized on the server, and deserialized on the client. Then when accessing myProvider on the client, it will return the original value from the server.

The syncWith() method takes a unique String key to identify the provider, and an optional Codec<T, Object?>? codec parameter for converting the provider value of type T to/from a serializable value. The codec parameter is not needed for serializable values, like primitives (String, numbers, bool, etc.) or Maps and Lists of these.

The following provider types support syncing: NotifierProvider, AsyncNotifierProvider, Provider, FutureProvider, StreamProvider and StateProvider.

Awaiting Async Providers

If a synced provider is a FutureProvider, StreamProvider or AsyncNotifier, the ProviderScope will wait for the providers future to complete before building its child (on the server). Therefore reading the provider inside the subtree will already return the completed value.

Sync Overrides and Scoping

In detail, the process of syncing a provider between server and client works like this:

  • During pre-rendering on the server, the value of a synced provider is read, serialized and embedded into the rendered html.
  • Together with the html it is then sent to the client.
  • When the client first builds your component containing the ProviderScope, the embedded provider value is read and deserialized.
  • The provider is then overridden with the received value, using the normal .overrideWith() feature of Riverpod.
  • Accessing a synced provider on the client will always return the overridden value and skip it's initial computation.

It is important to note that overrides in Riverpod only propagate the provider chain when they are defined on the root ProviderScope, and the same applies to synced providers. Therefore, either make sure to define synced providers only on the root client-side ProviderScope, or set the dependencies option required for scoping providers.

Read more about scoping providers here.

Backstory: Why context extensions instead of Consumer?

Put simply: Because they are easier and more flexible to use and require less boilerplate.

The actual question would be why they are not part of flutter_riverpod in the first place. This is because mainly context.watch is not feasible in flutter_riverpod.

This is a limitation by Flutter itself, not Riverpod. There are long standing bugs (or missing features depending on how you view it) in flutter with InheritedWidget that make it impossible for context.watch to work properly. Recently there have been efforts by the creator of riverpod to fix these bugs, but without success.

As jaspr is basically a complete rewrite of flutters core framework, I went ahead and fixed these bugs and thereby making context.watch feasible.