Using riverpod
Last modified on Tue 29 Jun 2021

In this chapter we will explain how to use riverpod. Riverpod is like provider but different. Providers are most improtant piece of this library. A provider is an object that encapsulate a piece of state and allows listening to that state.

This guide assumes you already read through the official riverpod guide.

Different providers

There are many different provider types, depending on what class you extend. I'll just list some that we use:

Provider

You don't need to extend anything in this case, you are just providing simple object or primitive.

final counterProvider = Provider((ref) => Counter())

ChangeNotifier

ChangeNotifierProvider will give you ability to notify listeners which in turn will rebuild with new state.

final stockFilterProvider = ChangeNotifierProvider.autoDispose<StockFilterProvider>((ref) => StockFilterProvider());

class StockFilterProvider extends ChangeNotifier {
  String? filter;

  void onFilterChanged(String newFilter) {
    filter = newFilter;
    notifyListeners();
  }
}

The notifyListeners method will update UI listeners. UI can listen using ProviderListener, Consumer or hooks (which we prefer).

StateNotifier

The StateNotifier is part of separate package. It works very similar to ChangeNotifier except it defines one state T. This way it's very predictable that class will do only one thing and modify that state.

Setting state will call notifyListeners to update all the listeners.

class UserSpecificColorProvider extends StateNotifier<Color> {
  UserSpecificColorProvider() : super(Colors.grey.shade500);

  void setNewColor(Color newColor) {
    state = newColor;
  }
}

Other

There are many more of the providers, like StreamProvider or FutureProvider but they are rarely used. Also they are implemented with ChangeNotifier, so whatever the use case is it can also be done using the ChangeNotifier.

Modeling the provider

When creating the provider you should think how view and provider should interact, keeping this in mind:

UI --- (trigger events) ---> Provider

UI <---(reacts to) --- Provider states

This is explained in detail in chapter Communication between UI and Provider

Navigation subjects / Actions

Beside the events and states we recognize that sometimes UI will need to handle some actions that are not exactly view state. For example, UI will need to show next screen in navigator or show a dialog. These are not part of the view state and you cannot even show next screen while inside the build method.

The way we implement this is by using subjects. Inside your provider you can define subject and public stream.

final PublishSubject<NavigateToPermissionData> _navigateToPermissionSubject = PublishSubject();

Stream<NavigateToPermissionData> get navigateToPermission => _navigateToPermissionSubject;

Provider can then call this subject, esentially sending actions from provider to the UI.

/// In provider
_navigateToPermissionSubject.add(data);

/// In UI
provider.navigateToPermission.listen((data) {
  Navigator.of(context).push(PermissionScreen());
})

When to use this? Right now we want to make some balance between boilerplate and separating all of the responsiblities. So currently if navigation doesn't have any logic associated to it, it's OK to write navigation in the UI side, for example, having this kind of code:

Button(
  onPressed: () => Navigator.of(context).push(NewScreen.route()),
)

But if the screen navigation is not known upfront and we need to calculate it somehow, then we recommend extracting this logic to Provider and using navigation subjects as described. This way everything is separated and testable, otherwise it wouldn't be possible to purely unit test it.

Disposable Listener. Since you need to maintain lifecycle of stream subscriptions we recommend using widget like DisposableListener which we made to help us.

Hooks

Hooks are package that implements a new kind of object that manages a Widget life-cycles. They exist for one reason: increase the code-sharing between widgets by removing duplicates.

You can read more about hooks: https://pub.dev/packages/flutter_hooks

We use it with riverpod (hooks_riverpod package). This way we don't need Consumer widgets and our widget tree looks nicer:

  @override
  Widget build(BuildContext context) {
    final _provider = useProvider(someMyProvider);
    return Text('Result: ${provider.result}');
  }

Beside riverpod hooks, there are many other useful like useTextEditingController or useAnimationController...

Initializing providers

We recognize two kind of dependencies. The dependencies from other layers that are always the same, for example Interactors or other providers. These can be directly injected with GetIt:

final myProvider = ChangeNotifierProvider(
  (ref) => MyProvider(GetIt.I.get<MyInteractor>()),
);

For parameters that change in the runtime, for example based on which item user click, we send different itemId. For these we create init method:

MyProvider extends ChangeNotifier {

  late String _itemId;

  void init(String itemId) {
    _itemId = itemId;
  }

}

Init Provider hook. In the UI you can use Init Provider Hook to easily use and initialize provider:

  @override
  Widget build(BuildContext context) {
    final _testProvider = useInitProvider(testProvider, (TestProvider p) => p.init(id));

    // provider is inited and ready to be used here
    return Container(...);
  }

Family Modifier. If you've read through the official guide you can see it's possible to send parameters using family modifier. Problem with these is that every time you want to use provider you need to send that parameter.

final _provider = useProvider(myProvider(parameter))

This is issue because that means that every widget that uses the provider also needs to parameter. So we need to pass parameter around the screens and widget tree to be able to use it. Other issue is that riverpod will do equality check, so if you are passing object you might mistakenly create new provider.

We do use family in some cases where we want to use multiple providers of the same type based on some provider. For example:

final _exampleUs = useProvider(countryProvider(Country.usa))
final _exampleUk = useProvider(countryProvider(Country.uk))

Provider to provider communication

Provider can interact with other providers. Here's how can one provider depend on antoher:

One time read

final myProvider = ChangeNotifierProvider(
  (ref) => MyProvider(ref.read(otherProvider)),
);

This will return provider at that time, but if the otherProvider, the myProvider will not be updated. For that we use one of the following methods.

It's also possible to pass Reader to the provider so you can read it at a later time:

final myProvider = ChangeNotifierProvider(
  (ref) => MyProvider(ref.read),
);

class MyProvider extends ChangeNotifier {
  MyProvider(this.reader);

  final Reader read;

  void onUserClicked() {
    final result = read(otherProvider);
    // do something with the result
  }
}

Watch

final myProvider = ChangeNotifierProvider(
  (ref) => MyProvider(ref.watch(otherProvider)),
);

This will watch and rebuild the myProvider when the otherProvider changes. Be careful because this will create completely new instance of MyProvider which is something that you might not want. In that case use listeners directly like in next example.

Listener

final myProvider = ChangeNotifierProvider(
  (ref) => MyProvider(ref.read(otherProvider)),
);

class MyProvider extends ChangeNotifier {
  MyProvider(this._otherProvider) {
    _otherProvider.addListener(_otherProviderListener);
  };

  OtherProvider _otherProvider;

  void _otherProviderListener() {
    // Other provider changed
  }
}

Request Provider

In chapter Architecture we already talked how we use RequestProvider to handle request states.

Computed providers

Sometimes you can use providers just to compute value from existing providers resulting in nicer and shorter code:

final isInsuranceEnabled = Provider.autoDispose((ref) {
  final configuration = ref.watch(configurationProvider);
  final user = ref.watch(userProvider);
  final fieldValue = configuration.getInsurance();
  return user.hasInsurance && configuration.insuranceEnabled;
});

ProviderKeepAlive

If we use autoDispose provider modifier, the provider is automatically disposed if there are no listeners. This can be problematic if at some point provider gets disposed, but next screens needs that provider.

For this you can either remove autoDispose which means that provider will not be cleared! Or you can use ProviderKeepAlive widget on screen that has no listener. It will create a listener for that provider so it will keep it alive.