Communication between UI and Presenter
Last modified on Mon 20 Dec 2021

Events (UI -> Presenter)

First type of communication is when UI has to run some method on Presenter. We're calling these events just like Bloc and some other state management solutions would.

In presenter they are simple methods. For example:

RaisedButton(
  child: Text('Save'),
  onPressed: () {
    presenter.onSavePressed();
  }
)

Take a note on a naming. The events don't command on presenter, but say onSomethingHappened and presenter will decide what to do. Wrong naming would be presenter.saveProfile().

View state (UI <- Presenter)

Second type is when presenter is managing view state. The UI should observe that state and rebuild. Usually this is done by our presenters extending ChangeNotifier or StateNotifier:

    Consumer<MeetupScreenPresenter>(
      builder: (context, presenter, _) {
        return _MeetupList(list: presenter.state);
      },
    ),

With riverpod you can use hooks to flatten this: https://riverpod.dev/docs/concepts/reading/#useprovider-hooks_riverpod-only

Actions (UI <- Presenter)

The view state should describe how the view tree should be built. Sometimes presenter needs to communicate other things that don't fall under view tree of that widget. Most often these are:

For these we don't use view state, but create actions which the UI can listen to:

class ProfileScreenPresenter extends ChangeNotifier {
  final PublishSubject<Profile> _navigateToProfileDetailsSubject = PublishSubject();
  Stream<Profile> get navigateToProfileDetails => _navigateToProfileDetailsSubject;

  // ... view state

  void onProfileClicked() {
    if (profile.hasDetails()) {
      _navigateToProfileDetailsSubject.add(profile);
    }
  }

On the UI side you need to listen to the stream. You can use DisposableListener widget so you don't have to use StatefulWidget just for listening:

onListen: (compositeSubscription) {
  presenter.navigateToProfileDetails.listen((profile) {
    Navigator.of(context).push(ProfileDetailsScreen.route(profile));
  }).addTo(compositeSubscription)
}

Our recommended way of handling actions, which also reduces the boilerplate code, is to use action emitter.

Returning result back to presenter

Sometime with the navigation you want to return result of navigation or dialog back to the presenter. With previously explained setup, it's possible to do that with Completer.

class ProfileScreenPresenter extends ChangeNotifier {
  final PublishSubject<Completer<Profile>> _showChooseProfileDialogSubject = PublishSubject();
  Stream<NavigateToPermissionData> get showChooseProfileDialog => _showChooseProfileDialogSubject;

  // ... view state

  void onSetFavoriteProfileClicked() {
    final completer = Completer<Profile>();
    _showChooseProfileDialogSubject.add(completer));

    // Do something with the result
    final result = await completer.future;
    _saveFavoriteProfile(result);
  }

On the UI side using DisposableListener:

onListen: (compositeSubscription) {
  presenter.showChooseProfileDialog.listen((completer) {
     final result = _showChooseProfileDialog(context);
     completer.complete(result);
  }).addTo(compositeSubscription)
}