Turbo MVVM
A lightweight MVVM state management solution for Flutter, inspired by the FilledStacks stacked package. It simplifies managing view logic, state, and lifecycle in your Flutter applications.
Table of Contents
Features
- ViewModel Lifecycle Management: Automatic handling of
initialiseanddisposemethods. - Reactive UI Updates: Widgets rebuild efficiently when ViewModel state changes using
rebuild()orValueNotifier. - Event-Driven ViewModels: Sequential event processing via
TBaseEventViewModelwith stream-based queue. - Standalone Event Management: Use
TEventManagementoutside of ViewModels for event-driven logic. - State Management: Built-in support for managing
isInitialised,isBusy, andhasErrorstates. - Context Access: Safe access to
BuildContextwithin ViewModels. - Argument Passing: Simple mechanism to pass arguments to ViewModels during initialization.
- Global Busy Indicator: Centralized
TBusyServicefor managing application-wide busy states. - Helper Mixins: Utility mixins like
TBusyManagement,TErrorManagement, andTViewModelHelpers.
Installation
Add turbo_mvvm to your pubspec.yaml file:
dependencies:
flutter:
sdk: flutter
turbo_mvvm: ^1.3.0
Install the package:
flutter pub get
Import it:
import 'package:turbo_mvvm/turbo_mvvm.dart';
Core Concepts
TBaseViewModel
The base class for all ViewModels. Extend TBaseViewModel<A> where A is the type of arguments passed to the ViewModel.
Key features:
initialise(): Called once when the ViewModel is created. Set up data and listeners here.dispose(): Called when the ViewModel is removed. Clean up resources here.rebuild(): Notifies listeners (theTViewModelBuilder) to rebuild the UI.isMounted: Whether the parent widget is still in the widget tree.context: Safe access to theBuildContext.arguments: Arguments passed viaTViewModelBuilder.isInitialised: AValueListenable<bool>indicating initialisation completion.
TViewModelBuilder
A widget that creates and provides a TBaseViewModel to the widget tree. It listens to the ViewModel and rebuilds when rebuild() is called.
Key parameters:
viewModelBuilder: Returns an instance of your ViewModel.builder: Builds the UI withcontext,model,isInitialised, and optionalchild.argumentBuilder(optional): Provides arguments toinitialise.isReactive(default:true): Rebuilds onnotifyListeners().shouldDispose(default:true): Auto-callsdispose()on removal.minBusyDuration(optional): Shows global busy overlay during initialisation for at least this duration.onDispose(optional): Callback when the ViewModel is disposed.
TBaseEventViewModel
An event-driven ViewModel that processes events sequentially via a stream-based queue. Extend TBaseEventViewModel<ARGUMENTS, EVENT> where EVENT is an enum or sealed class representing your events.
Key features:
events: Declare the set of events this ViewModel handles.onEvent(EVENT): Return a handler for each event.emit(EVENT): Fire an event for processing.emitAsync<RESULT>(EVENT): Fire an event and await its result.- Events are processed sequentially via an internal queue.
- Error handling via
onEventError,onStreamError, andonDoneoverrides.
TEventManagement
A standalone abstract class providing the same event-driven architecture as TBaseEventViewModel, but without requiring a ViewModel. Use this when you need sequential event processing in services or other non-widget classes.
Mixins
TBusyManagement: AddsisBusy(ValueListenable<bool>),busyTitle,busyMessage, andsetBusy()for local busy states.TErrorManagement: AddshasError(ValueListenable<bool>),errorTitle,errorMessage, andsetError()for local error states.TViewModelHelpers: Provideswait()(delay) andaddPostFrameCallback().TBusyServiceManagement: Integrates with the globalTBusyServicefor application-wide busy states.
TBusyService
A singleton service for managing a global busy state. Useful for showing an overlay loading indicator across the entire app.
TBusyService.instance(): Access the singleton.TBusyService.initialise(): Configure default message, title, type, and timeout.setBusy(bool isBusy, ...): Set the global busy state.isBusyListenable: AValueListenable<TBusyModel>for changes.TBusyModel: ContainsisBusy,busyTitle,busyMessage,busyType, andpayload.TBusyType: Enum controlling the indicator appearance (indicator,indicatorBackdrop, etc.).
Usage Guide
1. Create your ViewModel
Extend TBaseViewModel and add your business logic with desired mixins.
import 'package:flutter/foundation.dart';
import 'package:turbo_mvvm/turbo_mvvm.dart';
class MyViewModel extends TBaseViewModel<String>
with TBusyManagement, TErrorManagement {
final ValueNotifier<int> _counter = ValueNotifier(0);
ValueListenable<int> get counter => _counter;
String? _greeting;
String? get greeting => _greeting;
@override
Future<void> initialise({bool doSetInitialised = true}) async {
_greeting = "Hello, $arguments!";
setBusy(true, message: "Loading data...");
await Future.delayed(const Duration(seconds: 2));
_counter.value = 10;
setBusy(false);
super.initialise(doSetInitialised: doSetInitialised);
}
void incrementCounter() {
_counter.value++;
}
@override
FutureOr<void> dispose() {
_counter.dispose();
disposeBusyManagement();
disposeErrorManagement();
super.dispose();
}
}
2. Connect ViewModel to your View
Use TViewModelBuilder in your widget.
import 'package:flutter/material.dart';
import 'package:turbo_mvvm/turbo_mvvm.dart';
class MyView extends StatelessWidget {
const MyView({super.key});
@override
Widget build(BuildContext context) {
return TViewModelBuilder<MyViewModel>(
viewModelBuilder: () => MyViewModel(),
argumentBuilder: () => "World",
builder: (context, model, isInitialised, child) {
if (!isInitialised) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
return Scaffold(
appBar: AppBar(title: Text(model.greeting ?? "Turbo MVVM")),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ValueListenableBuilder<int>(
valueListenable: model.counter,
builder: (context, count, _) {
return Text(
'Counter: $count',
style: Theme.of(context).textTheme.headlineMedium,
);
},
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: model.incrementCounter,
child: const Text('Increment'),
),
],
),
),
);
},
);
}
}
3. Event-Driven ViewModel
Use TBaseEventViewModel for ViewModels that process discrete events sequentially.
import 'package:turbo_mvvm/turbo_mvvm.dart';
enum CounterEvent { increment, decrement, reset }
class CounterViewModel extends TBaseEventViewModel<void, CounterEvent>
with TBusyManagement {
int _count = 0;
int get count => _count;
@override
Set<CounterEvent> get events => CounterEvent.values.toSet();
@override
TEventHandler<CounterEvent> onEvent(CounterEvent event) {
return switch (event) {
CounterEvent.increment => (_) async {
_count++;
rebuild();
},
CounterEvent.decrement => (_) async {
_count--;
rebuild();
},
CounterEvent.reset => (_) async {
_count = 0;
rebuild();
},
};
}
}
Emit events from the UI:
TViewModelBuilder<CounterViewModel>(
viewModelBuilder: () => CounterViewModel(),
builder: (context, model, isInitialised, child) {
return Column(
children: [
Text('Count: ${model.count}'),
ElevatedButton(
onPressed: () => model.emit(CounterEvent.increment),
child: const Text('Increment'),
),
],
);
},
);
4. Managing Busy State
The TBusyManagement mixin provides:
isBusy: AValueListenable<bool>for the busy state.setBusy(bool isBusy, {String? title, String? message}): Update the busy state.disposeBusyManagement(): Clean up resources (call indispose).
5. Handling Errors
The TErrorManagement mixin provides:
hasError: AValueListenable<bool>for the error state.setError(bool hasError, {String? title, String? message}): Update the error state.disposeErrorManagement(): Clean up resources (call indispose).
6. Global Busy State with TBusyService
For application-wide loading indicators:
void main() {
TBusyService.initialise(
busyMessageDefault: "Please wait...",
busyTypeDefault: TBusyType.indicatorBackdrop,
timeoutDurationDefault: const Duration(seconds: 30),
);
runApp(MyApp());
}
Use TBusyServiceManagement mixin in ViewModels to control the global busy state, or use TViewModelBuilder.minBusyDuration to show a busy overlay during initialisation.
7. Passing Arguments to ViewModel
Pass arguments via argumentBuilder and access them in initialise():
TViewModelBuilder<MyViewModel>(
viewModelBuilder: () => MyViewModel(),
argumentBuilder: () => "Hello",
builder: (context, model, isInitialised, child) { ... },
);
The argument is available as arguments inside the ViewModel after initState.
Example Project
Check the /example directory for a complete Flutter application demonstrating Turbo MVVM features.
Dependencies
provider: Used internally byTViewModelBuilderfor efficient state propagation.
Contributing
Contributions are welcome! Please open issues or pull requests on the GitHub repository.
License
This package is licensed under the MIT License. See the LICENSE file for details.
Libraries
- data/abstracts/t_base_event_view_model
- data/abstracts/t_base_view_model
- data/abstracts/t_event_management
- data/constants/turbo_mvvm_defaults
- data/enums/t_busy_type
- data/mixins/t_busy_management
- data/mixins/t_busy_service_management
- data/mixins/t_error_management
- data/mixins/t_view_model_helpers
- data/models/t_busy_model
- services/t_busy_service
- turbo_mvvm
- A lightweight MVVM state management solution for Flutter.
- typedefs/t_event_handler
- utils/t_completer_queue
- widgets/t_view_model_widget