Guides

Using with Riverpod

This is an exploration of using Flutter Forge alongside Riverpod.


Providing a Flutter Forge Store

In Flutter Forge we need to give a store to our component widget when we construct it. This is a requirement of Flutter Forge to facilitate its core tenets.

The question you are often faced with during the process of integrating a Flutter Forge component is, "Where does the store for this component widget come from?"

If the component widget is being used within another Flutter Forge component widget then the answer is simple. The store comes from scoping the parenting store down to the child store. Details on this can be found in the Composing Components guide.

If the component widget is not being used within another Flutter Forge component widget. We need to look a little closer at where it is being integrated and what its dependencies are.

Flutter Forge Managed Data

For example let's say that we are integrating a Flutter Forge component pretty high up in the hierarchy of the app. Maybe a screen for changing your notification preferences within the application.

We decide for the behavior we want, that the component's store needs to be a global. We quickly start constructing a global instance of a store for this screen. However, we soon reach the environment parameter where you have to construct the environment for this component. You quickly realize that you are stuck because you need to inject a function that uses a Notifications Preferences Client that is currently exposed in the application via a Riverpod provider, but you don't have access to ref.

Don't stress, all you have to do is simply provide the global store via a Riverpod provider so that you have access to ref. This might look something like the following.

final notificationPreferencesStore = Provider<Store<NotifiactionPreferencesState, NotificationPreferencesEnvironment, NotificationPreferencesAction>>((ref) {
	final initialState = const NotificationPreferencesState(notificationsEnabled: AsyncState.initial());
	final environment = NotificationPreferencesEnvironment(
          fetchPreferences: ref.read(notificationPreferencesClientProvider).getUserPreferences,
          updatePreference: (String key, Map<String, bool> currentState) => ref
              .read(notificationPreferencesClientProvider)
              .updateUserNotificationPreferences(key, currentState));
	return Store(
    	initialState: initialState,
    	reducer: notificationPreferencesReducer,
    	environment: environment,
	);
});

In the above we can see how this gives us access to ref, enabling us to resolve dependencies provided by Riverpod within our store.

It is worth recognizing that this is a side effect of Riverpod, and its lack of facilitating resolving dependencies outside other providers or Riverpod widgets. Most libraries/frameworks that provide dependency injection work similarly, where they expect you to be all in or all out in terms of usage. By providing our Flutter Forge store via Riverpod provider, we are accepting the design constraints of Riverpod and continuing the usage by making our store a Riverpod dependency injectable thing.

This enables us to use it where we integrate the component within a Riverpod widget in the widget tree. This might look something like the following.

return NotificationPreferencesScreen(
	store: ref.read(notificationPreferencesStore),
	notificationConfigs: getNotificationPreferencesConfiguration(context),
);

Riverpod Managed Data

In other situations you have your data managed by some kind of Riverpod provider to facilitate fetching, caching, and notifing about changes in that data. You may however want this data to be used as the state for a Flutter Forge component.

RiverpodWrappedFlutterForgeComponent

To facilitate this we have whipped up RiverpodWrappedFlutterForgeComponent, a Riverpod widget that facilitates wrapping a Flutter Forge component. Note: This is not part of the core Flutter Forge library as we didn't want to add Riverpod as a dependency just for this use case. Especially since not everyone is using Riverpod. So simply copy and paste it into your code base to use it.

/// Use a Flutter Forge component inside a Riverpod widget
///
/// This is a RiverpodWidget that wraps a Flutter Forge component and
/// provides the interface to define the mapping between the Flutter Forge
/// components Actions & State to Riverpod side of the world while also
/// observing the provided provider for changes and rebuilding the Flutter
/// Forge component when it changes.
///
/// provider - the Riverpod provider that will be observed for state changes and propagated to the Flutter Forge component
/// actionHandler - function responsible for interpreting the State and Action and handling it, could be by calling a Riverpod Notifier method, or anything
/// environment - the environment of the Flutter Forge component to faciliate dependency injection
/// reducer - optionally provider a Flutter Forge reducer for the component if you want its default logic, otherwise only the actionHandler will be used
@immutable
class RiverpodWrappedFlutterForgeComponent<S extends Equatable, E,
    A extends ReducerAction> extends ConsumerWidget {
  final ProviderBase<S> provider;
  final Reducer<S, E, A>? reducer;
  final Function(WidgetRef ref, S state, A action) actionHandler;
  final E environment;
  final ComponentWidget<S, E, A> Function(Store<S, E, A> store) builder;

  const RiverpodWrappedFlutterForgeComponent(
      {super.key,
      required this.provider,
      required this.actionHandler,
      required this.environment,
      this.reducer,
      required this.builder});

  @override
  Widget build(context, ref) {
    final state = ref.watch(provider);
    final store = Store(
        initialState: state,
        reducer: reducer != null
            ? Reducer.combine(reducer!, riverpodReducer(ref, actionHandler))
            : riverpodReducer(ref, actionHandler),
        environment: environment);
    return builder(store);
  }

  Reducer<S, E, A> riverpodReducer(
      WidgetRef ref, Function(WidgetRef ref, S state, A action) actionHandler) {
    return Reducer<S, E, A>((state, action) {
      actionHandler(ref, state, action);
      return ReducerTuple(state, []);
    });
  }
}

The above is a class to help wrap a Flutter Forge widget in a Riverpod widget and provide structure & guidance in terms of how to build a mapping between the Flutter Forge component's Actions & State to the Riverpod side of the world while also observing the given provider for changes and rebuilding the Flutter Forge component when it changes.

To use it we simply construct it, giving it the following arguments.

  • provider - the Riverpod provider that it will observe for triggering rebuilds of the widget
  • actionHandler - a function responsible for mapping Actions to mutations against the Riverpod provider managed state. Similar to a reducer but in Riverpod land
  • environment - the environment for the Flutter Forge component being wrapped
  • reducer? - an optional argument to provide reducer logic to happen prior to the actionHandler
  • builder - a function responsible for producing the wrapped Flutter Forge widget given a store

The following is an example of what the construction of this might look like.

RiverpodWrappedFlutterForgeComponent(
	provider: countNotifierProvider,
	actionHandler: ourRiverpodActionHandler,
	environment: Environment(getName: () => "someString"),
	reducer: integrateWithRiverpodReducer,
	builder: (store) {
	  return IntegrateWithRiverpodComponentWidget(store: store);
	})

The following is a more complete example with a Riverpod NotifierProvider being used to manage the data.

class CountNotifier extends Notifier<State> {
  @override
  State build() {
    return const State(count: 0, name: '');
  }

  addFive() {
    state = State(count: state.count + 5, name: '');
  }

  increment() {
    state = State(count: state.count + 1, name: '');
  }
}

final countNotifierProvider =
    NotifierProvider<CountNotifier, State>(CountNotifier.new);

ourRiverpodActionHandler(
    WidgetRef ref, State state, IntegrateWithRiverpodAction action) {
  if (action is Increment) {
    ref.read(countNotifierProvider.notifier).increment();
  }
}

@immutable
class MyRiverpodReadonlyWidget extends ConsumerWidget {
  const MyRiverpodReadonlyWidget({super.key});

  @override
  Widget build(context, ref) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Load On Init Component'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            RiverpodWrappedFlutterForgeComponent(
                provider: countNotifierProvider,
                actionHandler: ourRiverpodActionHandler,
                environment: Environment(getName: () => "someString"),
                reducer: integrateWithRiverpodReducer,
                builder: (store) {
                  return IntegrateWithRiverpodComponentWidget(store: store);
                }),
            OutlinedButton(
                onPressed: () =>
                    ref.read(countNotifierProvider.notifier).addFive(),
                child: const Text("parent riverpod increment by 5"))
          ],
        ),
      ),
    );
  }
}
Previous
Overriding Component UI