ark_mvp_flutter 1.0.0
ark_mvp_flutter: ^1.0.0 copied to clipboard
Flutter widgets and BuildContext integration for Ark MVP presenters.
example/main.dart
import 'dart:async';
import 'package:ark_mvp_flutter/ark_mvp_flutter.dart';
import 'package:flutter/material.dart';
import 'package:usecase_forge/usecase_forge.dart';
void main() => runApp(const CounterExampleApp());
final class CounterExampleApp extends StatelessWidget {
const CounterExampleApp({super.key});
@override
Widget build(final BuildContext context) => const MaterialApp(
debugShowCheckedModeBanner: false,
home: CounterFeature(),
);
}
final class CounterFeature extends StatefulWidget {
const CounterFeature({super.key});
@override
State<CounterFeature> createState() => _CounterFeatureState();
}
final class _CounterFeatureState extends State<CounterFeature> {
late final CounterUseCase _counter;
late final ModelBinding<CounterModel> _binding;
@override
void initState() {
super.initState();
_counter = CounterUseCase();
_binding = ModelBinding<CounterModel>(
read: () => CounterModel(
snapshot: _counter.state,
increment: (final int amount) {
_counter.add(IncrementCounter(amount));
},
),
changes: <Stream<Object?>>[_counter.stream],
);
}
@override
Widget build(final BuildContext context) =>
MvpView<CounterModel, CounterViewState, CounterEffect, CounterPresenter>(
model: _binding,
createPresenter: CounterPresenter.new,
onEffect: (final BuildContext context, final CounterEffect effect) {
switch (effect) {
case CounterLimitReached():
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('The counter limit is 10.')),
);
}
},
builder:
(
final BuildContext context,
final CounterViewState viewState,
final CounterPresenter presenter,
) => _CounterView(
viewState: viewState,
onIncrement: viewState.canIncrement
? presenter.increment
: presenter.explainLimit,
onToggleDetails: presenter.toggleDetails,
),
);
@override
void dispose() {
unawaited(_counter.close());
super.dispose();
}
}
final class _CounterView extends StatelessWidget {
const _CounterView({
required this.viewState,
required this.onIncrement,
required this.onToggleDetails,
});
final CounterViewState viewState;
final VoidCallback onIncrement;
final VoidCallback onToggleDetails;
@override
Widget build(final BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Ark MVP · one UseCase')),
body: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 420),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text(
viewState.valueLabel,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.displayMedium,
),
const SizedBox(height: 8),
Text(viewState.statusLabel, textAlign: TextAlign.center),
const SizedBox(height: 24),
FilledButton(
onPressed: onIncrement,
child: Text(
viewState.canIncrement ? 'Increment' : 'Explain limit',
),
),
TextButton(
onPressed: onToggleDetails,
child: Text(
viewState.detailsExpanded ? 'Hide details' : 'Show details',
),
),
if (viewState.detailsExpanded)
const Text(
'Business value comes from UseCaseSnapshot. Formatting, '
'button policy, and this local expansion state come from '
'Presenter.',
textAlign: TextAlign.center,
),
],
),
),
),
),
);
}
final class CounterState {
const CounterState(this.value);
final int value;
}
final class IncrementCounter extends UseCaseCommand {
const IncrementCounter(this.amount);
final int amount;
}
final class CounterUseCase extends UseCase<CounterState> {
CounterUseCase() : super(initialState: const CounterState(0)) {
registerCommand<IncrementCounter>(_increment);
}
Future<void> _increment(
final IncrementCounter command,
final UseCaseExecutionContext<CounterState> context,
) async {
await Future<void>.delayed(const Duration(milliseconds: 150));
final int next = context.snapshot.state.value + command.amount;
context.publish(CounterState(next > 10 ? 10 : next));
}
}
final class CounterModel {
const CounterModel({required this.snapshot, required this.increment});
final UseCaseSnapshot<CounterState> snapshot;
final void Function(int amount) increment;
}
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();
}
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(final BuildContext context) {
final UseCaseSnapshot<CounterState> snapshot = model.snapshot;
final MaterialLocalizations localizations = MaterialLocalizations.of(
context,
);
return CounterViewState(
valueLabel: localizations.formatDecimal(snapshot.state.value),
statusLabel: switch (snapshot.phase) {
UseCaseExecutionPhase.processing => 'Processing command…',
UseCaseExecutionPhase.finished => 'Command completed',
_ => 'Ready',
},
canIncrement:
snapshot.state.value < 10 &&
snapshot.phase != UseCaseExecutionPhase.processing,
detailsExpanded: _detailsExpanded,
);
}
}