ark_mvp_flutter 1.0.0 copy "ark_mvp_flutter: ^1.0.0" to clipboard
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,
    );
  }
}
0
likes
160
points
261
downloads

Documentation

Documentation
API reference

Publisher

verified publisherarktelos.dev

Weekly Downloads

Flutter widgets and BuildContext integration for Ark MVP presenters.

Homepage
Repository (GitLab)
View/report issues

Topics

#architecture #flutter #mvp #presentation

License

Apache-2.0 (license)

Dependencies

ark_mvp, flutter

More

Packages that depend on ark_mvp_flutter