Руководства
Несколько UseCase на одном экране
Объединение независимых снимков на границе представления без переноса атомарной бизнес-координации в MVP.
Несколько UseCase на одном экране
Экран профиля может загружать данные и сохранять изменения через разные UseCase. MVP способен представить оба процесса, не объявляя их единой бизнес-транзакцией.
Итоговый поток выглядит так:
LoadProfileUseCase.state ─┐
├─> ProfileModel -> ProfilePresenter -> ProfileViewState
SaveProfileUseCase.state ─┘ └──────────> ProfileSaved effect
1. Поместите оба снимка в Model
final class ProfileModel {
const ProfileModel({
required this.profile,
required this.save,
required this.reload,
required this.saveName,
});
final UseCaseSnapshot<ProfileState> profile;
final UseCaseSnapshot<SaveProfileState> save;
final void Function() reload;
final void Function(String name) saveName;
}
Model содержит снимки и разрешённые операции, но не экземпляры UseCase. Presenter не может регистрировать команды, закрывать UseCase или обходить его публичную входную границу.
2. Наблюдайте оба источника
ModelBinding.changes содержит оба Stream, повторяющих последнее значение. После сигнала любого источника read() получает оба актуальных снимка. Presenter работает с полным состоянием функции, а не с частичным событием.
final ModelBinding<ProfileModel> binding = ModelBinding<ProfileModel>(
read: () => ProfileModel(
profile: loadProfile.state,
save: saveProfile.state,
reload: () => loadProfile.add(const LoadProfile()),
saveName: (name) => saveProfile.add(SaveProfile(name)),
),
changes: <Stream<Object?>>[loadProfile.stream, saveProfile.stream],
);
3. Постройте единые правила экрана
Presenter объединяет загрузку, сохранение, имя, статус и доступность кнопок в один ProfileViewState. View не определяет, какой UseCase занят, и не объединяет две подписки.
final class ProfilePresenter
extends FlutterPresenter<ProfileModel, ProfileViewState, ProfileEffect> {
void reload() => model.reload();
void saveExampleName() => model.saveName('Grace Hopper');
@override
ProfileViewState buildViewState(BuildContext context) {
final bool loading =
model.profile.phase == UseCaseExecutionPhase.processing;
final bool saving = model.save.phase == UseCaseExecutionPhase.processing;
return ProfileViewState(
title: model.profile.state.name.isEmpty
? 'Profile'
: model.profile.state.name,
status: loading
? 'Загрузка профиля…'
: saving
? 'Сохранение ${model.save.state.name}…'
: 'Профиль готов',
isLoading: loading,
isSaving: saving,
canSave: !loading && !saving,
);
}
}
4. Публикуйте эффект только при значимом переходе
Повтор последнего состояния и явное начальное чтение могут дать эквивалентные снимки. Перед ProfileSaved сравните предыдущую и текущую фазы сохранения. Не публикуйте эффект безусловно из onModelChanged.
@override
void onModelChanged(ProfileModel previous, ProfileModel current) {
final bool saveJustFinished =
previous.save.phase != UseCaseExecutionPhase.finished &&
current.save.phase == UseCaseExecutionPhase.finished &&
current.save.result == UseCaseExecutionResult.completed;
if (saveJustFinished) {
emitEffect(ProfileSaved(current.save.state.name));
}
}
5. Подключите View к составной границе
MvpView<ProfileModel, ProfileViewState, ProfileEffect, ProfilePresenter>(
model: binding,
createPresenter: ProfilePresenter.new,
onEffect: (context, effect) {
if (effect case ProfileSaved(:final name)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$name сохранён.')),
);
}
},
builder: (context, viewState, presenter) => ProfileScreenBody(
viewState: viewState,
onReload: presenter.reload,
onSave: presenter.saveExampleName,
),
)
ProfileScreenBody получает готовые к отображению значения и обратные вызовы. Он
не импортирует UseCase Forge и не анализирует фазы выполнения.
6. Оставьте бизнес-координацию в бизнес-слое
Агрегация для экрана корректна, пока загрузка и сохранение остаются независимыми операциями. Если они обязаны выполняться атомарно, требуется единая координирующая бизнес-граница. Ark MVP не запрещает такое решение, но и не навязывает его.
7. Закройте настоящих владельцев
Владелец функции закрывает оба UseCase. MvpView закрывает один Presenter.
Model и ViewState остаются неизменяемыми значениями без ресурсов.