Tutorials

Use an independent application state manager

Host Ark MVP without UseCase Forge and expose a stable ModelBinding through BuildContext.

v1.1.0Intermediate

Use an independent application state manager

This scenario uses one ordinary immutable application state and a broadcast Stream. No UseCase Forge type appears in the feature implementation.

Use this form for application-level state such as an authenticated session, language choice, or a runtime feature set. The state manager owns business state; MVP only adapts the part needed by one screen.

1. Expose state and changes

The manager owns current ApplicationState, publishes change signals, and provides named operations such as toggleTheme() or selectLanguage().

dart
final class ApplicationStateManager {
  final StreamController<ApplicationState> _changes =
      StreamController<ApplicationState>.broadcast();

  ApplicationState _state = const ApplicationState(
    userName: null,
    language: 'en',
  );

  ApplicationState get state => _state;
  Stream<ApplicationState> get changes => _changes.stream;

  void toggleSession() {
    _state = ApplicationState(
      userName: _state.userName == null ? 'Ada' : null,
      language: _state.language,
    );
    _changes.add(_state);
  }

  Future<void> close() => _changes.close();
}

2. Build the ModelBinding

Composition reads the current application state and captures only the operations this screen needs. The binding observes the manager Stream.

dart
final ModelBinding<ApplicationModel> binding =
    ModelBinding<ApplicationModel>(
  read: () => ApplicationModel(
    state: manager.state,
    toggleSession: manager.toggleSession,
    toggleLanguage: manager.toggleLanguage,
  ),
  changes: <Stream<Object?>>[manager.changes],
);

Model does not expose the manager itself. That keeps the screen contract small and prevents Presenter from reaching unrelated application operations.

3. Provide the binding through context

dart
MvpModelProvider<ApplicationModel>.value(
  binding: applicationBinding,
  child: const ApplicationScreen(),
)

A descendant MvpView<ApplicationModel, ...> can omit its direct model argument. Exact Model type determines lookup; interfaces are not registered automatically.

dart
MvpView<ApplicationModel, ApplicationViewState, Never,
    ApplicationPresenter>(
  createPresenter: ApplicationPresenter.new,
  builder: (context, viewState, presenter) => ApplicationScreenBody(
    greeting: viewState.greeting,
    environmentLabel: viewState.environmentLabel,
    sessionAction: viewState.sessionAction,
    languageAction: viewState.languageAction,
    onSessionPressed: presenter.toggleSession,
    onLanguagePressed: presenter.toggleLanguage,
  ),
)

Never is the effect type because this screen has no one-shot presentation event. It does not mean every screen should avoid effects.

4. Use presentation context safely

FlutterPresenter.buildViewState(context) can read Theme, locale, and text direction while deriving ViewState. It must not cache that context. Actions receive explicit parameters or invoke allowed Model operations.

dart
@override
ApplicationViewState buildViewState(BuildContext context) {
  final bool russian = model.state.language == 'ru';
  final Brightness brightness = Theme.of(context).brightness;
  final TextDirection direction = Directionality.of(context);
  return ApplicationViewState(
    greeting: model.state.userName == null
        ? (russian ? 'Гость' : 'Guest')
        : (russian
            ? 'Здравствуйте, ${model.state.userName}'
            : 'Hello, ${model.state.userName}'),
    sessionAction: model.state.userName == null
        ? (russian ? 'Войти' : 'Sign in')
        : (russian ? 'Выйти' : 'Sign out'),
    languageAction: russian ? 'Switch to English' : 'Переключить на русский',
    environmentLabel:
        'Theme: ${brightness.name}; direction: ${direction.name}',
  );
}

BuildContext is read during derivation because Theme and direction belong to the Flutter presentation environment. Model remains independent of Flutter, and Presenter never stores the context.

5. Preserve ownership

The surrounding widget creates and closes the manager. MvpModelProvider.value exposes the binding but does not acquire ownership of the manager. MvpView creates and closes Presenter.

dart
@override
void dispose() {
  unawaited(manager.close());
  super.dispose();
}

This example demonstrates the intended universality: Ark MVP defines the presentation boundary, while the application chooses its business-state mechanism.