Tutorials
Combine several UseCases for one screen
Aggregate independent snapshots at the presentation boundary while leaving atomic business coordination outside MVP.
Combine several UseCases for one screen
A profile screen can load data and save edits through separate UseCases. MVP may present both without pretending they form one business transaction.
The resulting flow is:
LoadProfileUseCase.state ─┐
├─> ProfileModel -> ProfilePresenter -> ProfileViewState
SaveProfileUseCase.state ─┘ └──────────> ProfileSaved effect
1. Put both snapshots in 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 contains snapshots and allowed operations, not the UseCase instances. Presenter therefore cannot register commands, close a UseCase, or bypass its public input boundary.
2. Observe both sources
ModelBinding.changes contains both replay-latest streams. After either emits, read() obtains both current snapshots. Presenter receives one complete feature input instead of a partial event.
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. Derive one screen policy
Presenter can combine loading, saving, current name, status text, and button policy into one ProfileViewState. The View never asks which UseCase is busy and does not merge two subscriptions.
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
? 'Loading profile…'
: saving
? 'Saving ${model.save.state.name}…'
: 'Profile is ready',
isLoading: loading,
isSaving: saving,
canSave: !loading && !saving,
);
}
}
4. Emit effects only for meaningful transitions
Replay-latest streams and explicit initial reads can produce equivalent snapshots. Compare previous and current save phases before emitting ProfileSaved; do not emit it unconditionally from 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. Attach one View to the composed boundary
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 was saved.')),
);
}
},
builder: (context, viewState, presenter) => ProfileScreenBody(
viewState: viewState,
onReload: presenter.reload,
onSave: presenter.saveExampleName,
),
)
ProfileScreenBody consumes presentation-ready values and callbacks. It does
not import UseCase Forge or inspect execution phases.
6. Keep business coordination where it belongs
This screen-level aggregation is valid when loading and saving remain independent operations. If both must commit atomically, create one coordinating business boundary. Ark MVP neither requires nor manufactures that UseCase.
7. Close the actual owners
The feature owner closes both UseCases. MvpView closes one Presenter. Model and ViewState are immutable values and own no resources.