MVVM Framework

A Flutter library for implementing the Model-View-ViewModel (MVVM) architectural pattern. This framework simplifies state management and promotes a clean separation of concerns in Flutter applications.

Why MVVM?

  • Enforced separation — business logic lives in the ViewModel; the UI is kept dumb and declarative.
  • Improved readability — structured, predictable code that is easy to navigate and maintain.
  • Enhanced testability — ViewModels are plain Dart classes, trivial to unit-test without a widget tree.
  • Centralized state — a single Model object per screen drives both the view and side effects.

Features

  • Base classesModel<T>, ViewModel<M>, ViewWidget<VM>, ViewListener<M, VM>, and the MVVM widget.
  • ViewListener pattern — typed hooks (onSuccess, onFailure, onInitial, onLoading) for side effects, cleanly separated from business logic.
  • Built on flutter_bloc — all BLoC features remain available; the framework adds MVVM conventions on top.
  • safeExecute — wraps any async action; catches exceptions and emits ViewStatusFailure automatically. Pass withLoading: true to also emit the loading state before the action runs.
  • Managed stream subscriptionssubscribeStream(stream, onData: ...) auto-cancels in close().
  • Layered error dispatch — four priority levels from a built-in AlertDialog fallback up to per-ViewModel handlers (see Error Handling).
  • Cross-platform — iOS, Android, macOS, Windows, Linux, and Web.

Examples

Refer to the Basic Example for a quick start, or browse the full Example README for more scenarios.

Code Snippets

Copy mvvm.code-snippets into your project's .vscode/ folder.

Trigger Generates
mvvm Complete MVVM component
mvvm view View widget
mvvm view-model ViewModel
mvvm listener ViewListener
mvvm model Model class
mvvm test Test setup

The snippets assume freezed, injectable, and mockito are already in your project.

Error Handling

Errors flow through a four-tier dispatch, evaluated in priority order (highest first):

Priority Mechanism Scope
1 (highest) ViewModel.registerErrorHandler<T>() Per-ViewModel, overrides everything
2 GenericErrorManager.register<T>() App-wide, type-specific
3 GenericErrorManager.setFallback() / ConfigMVVM.initialize(errorFallback: …) App-wide catch-all
4 (lowest) Built-in AlertDialog Framework default

Typical setup

// errors_setup.dart — call before ConfigMVVM.initialize()
void configureErrors() {
  GenericErrorManager.register<NetworkException>(
    (e, stackTrace, ctx) => ctx.scaffoldMessenger.showSnackBar(MySnackBar(e.message)).closed,
  );
}

// main.dart
void main() {
  configureErrors();
  ConfigMVVM.initialize(
    // plug in your design-system dialog for unregistered exceptions
    errorFallback: (cause, stackTrace, ctx) => MyErrorDialog(ctx.context, cause.toString()).show(),
  );
  runApp(const MyApp());
}

Per-ViewModel override

class MyVM extends ViewModel<MyModel> {
  @override
  Future<void> onInit() async {
    registerErrorHandler<InvalidLocationException>(
      () => emit(state.copyWith(isInvalidLocation: true)),
    );
  }
}

Domain code throws plain exceptions. safeExecute catches them and wraps them in ViewStatusError(cause: exception); access the original exception via error.cause.

ViewModel Lifecycle

Hook When called Errors caught?
onInit() Once, immediately after the VM is created Yes — safeExecute
didUpdateWidget(newModel) Each time externalModel changes in the parent widget Yes — safeExecute
onDispose() Just before close() (after stream subscriptions cancelled) No

safeExecute examples

// Emit loading, run action, catch exceptions → ViewStatusFailure
Future<void> refresh() => safeExecute(_fetchData, withLoading: true);

// onError is async — awaited before the failure state is emitted
Future<void> fetch() => safeExecute(
  _loadData,
  onError: (error) async => _logger.log(error.cause),
  withLoading: true,
);

// Stream subscription cancelled automatically in close()
@override
Future<void> onInit() async {
  subscribeStream(locationService.stream, onData: _onLocation);
}

Contributing

Contributions are welcome!

  1. Open an issue to discuss the change (e.g. issue #123).
  2. Create a feature branch, make your changes, and run dart format ..
  3. Commit using Conventional Commits: <type>[!]: <description> #<issue>
    • Common types: fix, feat, docs, refactor, test, chore
    • Add ! before : for breaking changes (e.g. feat!: ...)
  4. Open a Pull Request.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Libraries

mvvm_framework