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
Modelobject per screen drives both the view and side effects.
Features
- Base classes —
Model<T>,ViewModel<M>,ViewWidget<VM>,ViewListener<M, VM>, and theMVVMwidget. - 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 emitsViewStatusFailureautomatically. PasswithLoading: trueto also emit the loading state before the action runs.- Managed stream subscriptions —
subscribeStream(stream, onData: ...)auto-cancels inclose(). - Layered error dispatch — four priority levels from a built-in
AlertDialogfallback 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, andmockitoare 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!
- Open an issue to discuss the change (e.g. issue #123).
- Create a feature branch, make your changes, and run
dart format .. - Commit using Conventional Commits:
<type>[!]: <description> #<issue>- Common types:
fix,feat,docs,refactor,test,chore - Add
!before:for breaking changes (e.g.feat!: ...)
- Common types:
- Open a Pull Request.
License
This project is licensed under the MIT License - see the LICENSE file for details.