Руководства

Функция на основе одного UseCase

Подключение снимка UseCase к Model, адаптация в Presenter и отрисовка через MvpView с явно переданным ModelBinding.

v1.1.0Средний уровень

Функция на основе одного UseCase

В этом счётчике UseCase Forge служит опциональным источником бизнес-состояния. MVP-пакеты от него не зависят: оба продукта соединяет композиция приложения.

Что будет создано

text
CounterUseCase владеет CounterState и выполнением Command
        ↓ ModelBinding читает полную CounterModel
CounterPresenter преобразует Snapshot + BuildContext в CounterViewState
        ↓
MvpView отображает состояние и однократно доставляет CounterEffect

Установите пакеты примера:

shell
flutter pub add ark_mvp:^1.1.0 ark_mvp_flutter:^1.1.0
flutter pub add usecase_forge:^1.1.0

UseCase уже предоставляет UseCaseSnapshot<CounterState> и принимает IncrementCounter. Руководство начинается на границе композиции представления; полный UseCase доступен в исходном примере.

1. Определите владельца бизнес-объекта

Внешний StatefulWidget создаёт CounterUseCase и закрывает его в dispose(). MvpView не присваивает себе это владение.

2. Опишите Model функции

dart
final class CounterModel {
  const CounterModel({
    required this.snapshot,
    required this.increment,
  });

  final UseCaseSnapshot<CounterState> snapshot;
  final void Function(int amount) increment;
}

Конкретный UseCase не пересекает границу. Model содержит актуальный снимок и одну разрешённую операцию.

3. Создайте стабильный ModelBinding

dart
late final ModelBinding<CounterModel> binding = ModelBinding(
  read: () => CounterModel(
    snapshot: counter.state,
    increment: (amount) => counter.add(IncrementCounter(amount)),
  ),
  changes: <Stream<Object?>>[counter.stream],
);

ModelBinding создаётся один раз на жизненный цикл функции. Новый экземпляр при каждом построении Flutter означал бы намеренное создание нового Presenter.

4. Адаптируйте состояние в Presenter

Сначала определите ViewState со всеми данными, необходимыми Widget:

dart
final class CounterViewState {
  const CounterViewState({
    required this.valueLabel,
    required this.statusLabel,
    required this.canIncrement,
    required this.detailsExpanded,
  });

  final String valueLabel;
  final String statusLabel;
  final bool canIncrement;
  final bool detailsExpanded;
}

sealed class CounterEffect {
  const CounterEffect();
}

final class CounterLimitReached extends CounterEffect {
  const CounterLimitReached();
}

Presenter преобразует значение и фазу выполнения в текст и правила взаимодействия. Во время построения ViewState допустимо обратиться к MaterialLocalizations.of(context): используется актуальный контекст, который нигде не сохраняется.

dart
final class CounterPresenter
    extends FlutterPresenter<CounterModel, CounterViewState, CounterEffect> {
  bool _detailsExpanded = false;

  void increment() => model.increment(1);
  void explainLimit() => emitEffect(const CounterLimitReached());

  void toggleDetails() {
    _detailsExpanded = !_detailsExpanded;
    invalidateView();
  }

  @override
  CounterViewState buildViewState(BuildContext context) {
    final snapshot = model.snapshot;
    final localizations = MaterialLocalizations.of(context);
    return CounterViewState(
      valueLabel: localizations.formatDecimal(snapshot.state.value),
      statusLabel: switch (snapshot.phase) {
        UseCaseExecutionPhase.processing => 'Выполнение Command…',
        UseCaseExecutionPhase.finished => 'Command завершён',
        _ => 'Готово',
      },
      canIncrement: snapshot.state.value < 10 &&
          snapshot.phase != UseCaseExecutionPhase.processing,
      detailsExpanded: _detailsExpanded,
    );
  }
}

Публичный increment() либо вызывает операцию Model, либо публикует CounterLimitReached. View не повторяет правило лимита.

5. Отобразите готовые значения

Передайте стабильный ModelBinding напрямую в MvpView. Параметр builder получает текущий ViewState и конкретный Presenter. Эффект обрабатывается с актуальным BuildContext:

dart
MvpView<CounterModel, CounterViewState, CounterEffect, CounterPresenter>(
  model: binding,
  createPresenter: CounterPresenter.new,
  onEffect: (context, effect) {
    switch (effect) {
      case CounterLimitReached():
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('Предел счётчика — 10.')),
        );
    }
  },
  builder: (context, state, presenter) => CounterView(
    value: state.valueLabel,
    status: state.statusLabel,
    onIncrement: state.canIncrement
        ? presenter.increment
        : presenter.explainLimit,
    onToggleDetails: presenter.toggleDetails,
  ),
)

CounterView получает строки, флаги и обратные вызовы. Он не может читать Model или выводить политику из UseCaseExecutionPhase.

6. Закройте созданный объект

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

MvpView закрывает сессию Presenter. Внешний StatefulWidget закрывает UseCase, потому что создал этот бизнес-объект.

7. Проверьте границы

  • View не имеет доступа к CounterModel;
  • Presenter не может отправить необъявленную Command;
  • MvpView владеет Presenter;
  • внешний виджет владеет UseCase и закрывает его;
  • изменение локали или темы сохраняет Presenter и перестраивает ViewState.

Запустите полный пример по ссылке и нажимайте кнопку до значения десять. Бизнес-изменения поступают через ModelBinding, Presenter заново формирует форматирование и доступность кнопки, а сообщение о лимите однократно доставляется как ViewEffect.