ark_mvp 0.1.0-dev.1 copy "ark_mvp: ^0.1.0-dev.1" to clipboard
ark_mvp: ^0.1.0-dev.1 copied to clipboard

Pure Dart foundations for Model-View-Presenter application features.

Ark MVP #

Pure Dart foundations for Model–View–Presenter features with explicit Model composition, presentation-state adaptation, one-time ViewEffects, and a bounded Presenter lifecycle.

Ark MVP does not prescribe a state manager, dependency-injection container, repository implementation, or UI framework. Application composition reads the business objects it already owns and exposes one immutable feature Model. A Presenter converts that Model into presentation-ready ViewState and publishes only the actions and transient effects that belong to the View.

Русская версия

0.1.0-dev.1 is a prerelease. The lifecycle model is implemented and tested, but public API details may change before the first stable version.

Why this package exists #

MVP is often reduced to a class called Presenter that merely forwards raw state to a widget. That leaves the View responsible for interpreting business phases, combining several state managers, formatting values, and deciding which operations are allowed. The class exists, but the adaptation boundary does not.

Ark MVP gives each layer a narrower responsibility:

  • application composition connects existing business objects to a feature;
  • Model is one immutable snapshot of everything the feature currently needs;
  • Presenter converts that snapshot into presentation-ready values and actions;
  • View renders ViewState and delegates user intent to Presenter methods;
  • ViewEffect carries a one-time UI request that must not become persistent state.

The API keeps Model out of View without adding runtime type checks: MvpView receives ViewState and Presenter, never Model. Dart cannot determine whether an application has placed a technical object inside its own Model or ViewState; that semantic boundary remains visible and reviewable in the feature types.

The complete flow #

Business objects owned by the application
  ├── one UseCase
  ├── several UseCases
  ├── application state manager
  └── any other typed business boundary
              │
              │ read complete immutable snapshot
              ▼
        ModelBinding<Model>
              │
              │ Model changed
              ▼
       Presenter<Model, ViewState, ViewEffect, Environment>
              │
              ├── builds ViewState
              ├── exposes feature actions
              ├── invalidates Presenter-local UI state
              └── emits one-time ViewEffects
              │
              ▼
             View

The arrows are intentionally one-directional. A View does not read the Model, UseCase, repository, or service. A Presenter does not own the business sources behind its ModelBinding. The composition root keeps those ownership decisions.

Installation #

dependencies:
  ark_mvp: ^0.1.0-dev.1
import 'package:ark_mvp/ark_mvp.dart';

Core concepts #

Model #

Ark MVP deliberately has no universal Model base class. The feature defines an ordinary immutable type whose fields express its actual business inputs and allowed operations.

final class CounterModel {
  const CounterModel({
    required this.value,
    required this.increment,
  });

  final int value;
  final void Function() increment;
}

This is not a domain entity and not a second state manager. It is a feature snapshot assembled at the presentation boundary. A more complex Model can contain several typed business snapshots and actions:

final class ProfileModel {
  const ProfileModel({
    required this.profile,
    required this.update,
    required this.session,
    required this.reload,
    required this.save,
  });

  final ProfileSnapshot profile;
  final UpdateSnapshot update;
  final SessionState session;
  final void Function() reload;
  final void Function(ProfileDraft draft) save;
}

The package cannot enforce deep immutability in Dart. The binding contract requires each read() result to remain a stable snapshot after it is returned.

ModelBinding #

ModelBinding<M> connects application-owned business state to the MVP runtime:

final ModelBinding<CounterModel> binding = ModelBinding<CounterModel>(
  read: () => CounterModel(
    value: counter.value,
    increment: counter.increment,
  ),
  changes: <Stream<Object?>>[
    counter.changes,
  ],
);

The values emitted by changes are signals, not partial Model updates. After every signal, the runtime calls read() and obtains a complete, internally consistent Model snapshot. This makes several heterogeneous streams possible without teaching Ark MVP what their event types mean.

The binding does not close the streams or the objects captured by read. If an application created a UseCase, state manager, or subscription, the application still owns its lifecycle.

Presenter #

Presenter is the adapter between Model and View. It receives the complete Model but exposes only presentation-ready ViewState and feature actions.

final class CounterViewState {
  const CounterViewState({
    required this.formattedValue,
    required this.canIncrement,
  });

  final String formattedValue;
  final bool canIncrement;
}

final class CounterPresenter extends Presenter<
    CounterModel,
    CounterViewState,
    CounterEffect,
    CounterEnvironment> {
  @override
  CounterViewState buildViewState(CounterEnvironment environment) {
    return CounterViewState(
      formattedValue: environment.formatNumber(model.value),
      canIncrement: model.value < 100,
    );
  }

  void increment() {
    if (model.value >= 100) {
      emitEffect(const CounterLimitReached());
      return;
    }
    model.increment();
  }
}

The View does not reproduce the value < 100 rule or number formatting. It receives formattedValue, canIncrement, and the increment() action.

ViewState #

ViewState is an application-defined immutable value. It contains durable presentation data for the current frame:

  • formatted text;
  • visibility and enabled-state flags;
  • selected items;
  • validation messages;
  • loading, empty, content, and error presentation variants;
  • other values that can be rebuilt repeatedly without changing meaning.

There is no package base class because different features need different fields. Keeping a concrete type also makes the View compile-time explicit.

ViewEffect #

ViewEffect represents a one-time request to the active View:

sealed class CounterEffect {
  const CounterEffect();
}

final class CounterLimitReached extends CounterEffect {
  const CounterLimitReached();
}

Effects are broadcast without retention or replay. A newly attached View does not repeat an old dialog, navigation request, snackbar, or focus action. If a value must survive rebuilding or reattachment, it belongs in ViewState instead.

Use Never as the effect type when a Presenter has no effects.

Pure Dart host #

PresenterSession exposes the framework-independent lifecycle. Most Flutter applications use MvpView from ark_mvp_flutter; another UI adapter or a pure Dart program can drive the same Presenter directly:

final CounterPresenter presenter = CounterPresenter();
final PresenterSession<
  CounterModel,
  CounterViewState,
  CounterEffect,
  CounterEnvironment
> session = PresenterSession(
  presenter: presenter,
  initialModel: binding.read(),
);

final effectSubscription = session.effects.listen(handleEffect);
final invalidationSubscription = session.invalidations.listen((_) {
  render(session.buildViewState(environment));
});

session.start();
render(session.buildViewState(environment));

final modelSubscriptions = binding.changes.map((source) {
  return source.listen((_) {
    session.updateModel(binding.read());
    render(session.buildViewState(environment));
  });
}).toList();

// Later, after the host stops presenting the feature:
for (final subscription in modelSubscriptions) {
  await subscription.cancel();
}
await effectSubscription.cancel();
await invalidationSubscription.cancel();
await session.close();

Subscribe before start(): onModelAttached is allowed to invalidate the View or emit an effect.

Presenter lifecycle #

One Presenter instance belongs to exactly one session:

  1. the runtime creates a Presenter and attaches the initial Model;
  2. output listeners subscribe;
  3. start() invokes onModelAttached once;
  4. Model signals produce updateModel(previous, current) transitions;
  5. the host asks buildViewState(environment) whenever presentation changes;
  6. close() invokes closePresenter, closes output channels, and detaches the Presenter.

A closed Presenter cannot be reused. Create a new instance for a new feature or binding lifecycle. This prevents stale local state and subscriptions from crossing feature boundaries.

Presenter-local state #

Small presentation-only state can live in Presenter: a temporary expanded section, a selected local tab, or a draft-display mode. After changing it, call invalidateView() so the host rebuilds ViewState.

Do not move business truth into Presenter. Data that must survive screen replacement, participate in domain decisions, or be shared with another feature belongs in a business state manager and therefore in Model.

What Ark MVP does not do #

Ark MVP intentionally does not provide:

  • dependency injection or a service locator;
  • repositories, DataSources, or transport adapters;
  • a business-state implementation;
  • a universal Intent hierarchy;
  • automatic ownership of UseCases or state managers;
  • navigation;
  • persistence;
  • reflection or code generation;
  • a global Model registry.

These omissions keep the MVP boundary usable with ordinary constructors, Ark DI, another DI solution, UseCase Forge, or an application-specific manager.

Examples and detailed guides #

  • Basic pure Dart Presenter
  • Application state manager composition
  • Architecture and responsibilities
  • Model composition
  • Lifecycle, ViewState, and ViewEffect

Flutter hosting and UseCase Forge composition are documented in ark_mvp_flutter.

0
likes
160
points
--
downloads

Documentation

Documentation
API reference

Publisher

verified publisherarktelos.dev

Pure Dart foundations for Model-View-Presenter application features.

Homepage
Repository (GitLab)
View/report issues

Topics

#architecture #mvp #presentation #state-management

License

Apache-2.0 (license)

Dependencies

meta

More

Packages that depend on ark_mvp