Using riverpod
Last modified on Mon 20 Dec 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 counterPresenter = Provider((ref) => Counter())

ChangeNotifier

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

final stockFilterPresenter = ChangeNotifierProvider.autoDispose<StockFilterPresenter>((ref) => StockFilterPresenter());

class StockFilterPresenter 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 UserSpecificColorPresenter extends StateNotifier<Color> {
  UserSpecificColorPresenter() : 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 presenter

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

UI --- (triggers events) ---> Presenter

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

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

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 presenter you can define subject and public stream.

final PublishSubject<NavigateToPermissionData> _navigateToPermissionSubject = PublishSubject();

Stream<NavigateToPermissionData> get navigateToPermission => _navigateToPermissionSubject;

Presenter can then call this subject, esentially sending actions from presenter to the UI.

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

/// In UI
presenter.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 Presenter 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 _presenter = useProvider(someMyPresenter);
    return Text('Result: ${_presenter.result}');
  }

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

Initializing presenters

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

final myPresenter = ChangeNotifierProvider(
  (ref) => MyPresenter(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:

MyPresenter 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 _testPresenter_ = useInitProvider(testPresenter, (TestPresenter p) => p.init(id));

    // presenter 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 _presenter_ = useProvider(myPresenter(parameter))

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

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

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

Presenter to presenter communication

Presenter can interact with other presenters. Here's how can one presenter depend on antoher:

One time read

final myPresenter = ChangeNotifierProvider(
  (ref) => MyPresenter(ref.read(otherPresenter)),
);

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

final myPresenter = ChangeNotifierProvider(
  (ref) => MyPresenter(ref.read),
);

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

  final Reader read;

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

This will return presenter at that time, but if the otherPresenter changes, the myPresenter will not be updated. For that we use one of the following methods.

Watch

final myPresenter = ChangeNotifierProvider(
  (ref) => MyPresenter(ref.watch(otherPresenter)),
);

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

Listener

final myPresenter = ChangeNotifierProvider(
  (ref) => MyPresenter(ref.read(otherPresenter)),
);

class MyPresenter extends ChangeNotifier {
  MyPresenter(this._otherPresenter) {
    _otherPresenter.addListener(_otherPresenterListener);
  };

  OtherPresenter _otherPresenter;

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

Request Provider

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

Computed presenters

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

final isInsuranceEnabledPresenter = Provider.autoDispose((ref) {
  final configuration = ref.watch(configurationPresenter);
  final user = ref.watch(userPresenter);
  final fieldValue = configuration.getInsurance();
  return user.hasInsurance && configuration.insuranceEnabled;
});

ProviderKeepAlive

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

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