Руководства
Функция на основе одного UseCase
Подключение снимка UseCase к Model, адаптация в Presenter и отрисовка через MvpView с явно переданным ModelBinding.
Функция на основе одного UseCase
В этом счётчике UseCase Forge служит опциональным источником бизнес-состояния. MVP-пакеты от него не зависят: оба продукта соединяет композиция приложения.
Что будет создано
CounterUseCase владеет CounterState и выполнением Command
↓ ModelBinding читает полную CounterModel
CounterPresenter преобразует Snapshot + BuildContext в CounterViewState
↓
MvpView отображает состояние и однократно доставляет CounterEffect
Установите пакеты примера:
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 функции
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
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:
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): используется актуальный контекст, который нигде не сохраняется.
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:
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. Закройте созданный объект
@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.