Veto is a lightweight and intuitive MVVM (Model-View-ViewModel) state management solution for Flutter, originally inspired by the FilledStacks stacked package. It simplifies managing view logic, state, and lifecycle in your Flutter applications.
- Features
- Installation
- Core Concepts
- Usage Guide
- Example Project
- Dependencies
- Contributing
- License
- Support
- Easy ViewModel Lifecycle Management: Automatic handling of
initialiseanddisposemethods. - Reactive UI Updates: Widgets rebuild efficiently when ViewModel state changes using
rebuild()orValueNotifier. - 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
BusyServicefor managing application-wide busy states. - Helper Mixins: Utility mixins like
BusyManagement,ErrorManagement, andViewModelHelpers. - Lightweight and Intuitive: Designed to be easy to learn and integrate.
-
Add
vetoto yourpubspec.yamlfile:dependencies: flutter: sdk: flutter veto: ^latest_version # Replace with the latest version from pub.dev
-
Install the package by running:
flutter pub get
-
Import the package in your Dart files:
import 'package:veto/veto.dart';
The heart of the Veto package. Your ViewModels should extend BaseViewModel<A>, where A is the type of arguments you want to pass to the ViewModel.
Key features:
initialise(): Called once when the ViewModel is created. Ideal for setting up data, listeners, etc.dispose(): Called when the ViewModel is no longer needed. Clean up resources here.rebuild(): Notifies listeners (typically theViewModelBuilder) to rebuild the UI.isMounted: A boolean getter to check if the associated View (widget) is currently in the widget tree.context: Provides safe access to theBuildContext.arguments: Holds arguments passed viaViewModelBuilder.isInitialised: AValueListenable<bool>that indicates ifinitialise()has completed.
A widget that builds and provides a BaseViewModel to the widget tree. It listens to the ViewModel and rebuilds its child widgets when rebuild() is called or when other ValueNotifiers within the ViewModel change.
Key parameters:
viewModelBuilder: A function that returns an instance of your ViewModel.builder: A function that builds your UI, receiving thecontext,model(your ViewModel instance),isInitialisedstatus, and an optionalchild.argumentBuilder(optional): A function to provide arguments to your ViewModel'sinitialisemethod.isReactive(default:true): If true, the builder will rebuild whennotifyListeners()(called byrebuild()) is invoked on the ViewModel.shouldDispose(default:true): If true, the ViewModel'sdispose()method will be called automatically.onDispose(optional): A callback executed when the ViewModel is disposed.
Veto provides several mixins to add common functionalities to your ViewModels:
BusyManagement: AddsisBusy(ValueListenable<bool>),busyTitle,busyMessage, andsetBusy()method for managing local busy states within a ViewModel.ErrorManagement: AddshasError(ValueListenable<bool>),errorTitle,errorMessage, andsetError()method for managing local error states.ViewModelHelpers: Provides utility methods likewait()(for delays) andaddPostFrameCallback().BusyServiceManagement: Integrates with the globalBusyServiceto manage application-wide busy states.
A singleton service for managing a global busy state. This is useful for showing an overlay loading indicator across the entire app.
BusyService.instance(): Access the singleton instance.BusyService.initialise(): Configure default busy message, title, type, and timeout.setBusy(bool isBusy, ...): Sets the global busy state.isBusyListenable: AValueListenable<BusyModel>to listen for changes in the global busy state.BusyModel: ContainsisBusy,busyTitle,busyMessage,busyType, andpayload.BusyType: Enum to control the appearance of the busy indicator (e.g.,indicator,indicatorBackdrop).
Here’s a step-by-step guide to using Veto:
Extend BaseViewModel and add your business logic, state variables, and any desired mixins.
import 'package:flutter/foundation.dart';
import 'package:veto/veto.dart';
class MyViewModel extends BaseViewModel<String> // String is the argument type
with BusyManagement, ErrorManagement {
final ValueNotifier<int> _counter = ValueNotifier(0);
ValueListenable<int> get counter => _counter;
String? _greeting;
String? get greeting => _greeting;
@override
Future<void> initialise() async {
_greeting = "Hello, ${arguments}!";
setBusy(true, message: "Loading data...");
await Future.delayed(const Duration(seconds: 2));
_counter.value = 10;
setBusy(false);
super.initialise();
debugPrint("MyViewModel Initialised with argument: $arguments");
}
void incrementCounter() {
_counter.value++;
}
void performFailableOperation() async {
setBusy(true);
setError(false);
try {
await Future.delayed(const Duration(seconds: 1));
throw Exception("Something went wrong!");
} catch (e, s) {
debugPrintStack(label: e.toString(), stackTrace: s);
setError(true, title: "Error", message: e.toString());
} finally {
setBusy(false);
}
}
@override
void dispose() {
_counter.dispose();
disposeBusyManagement();
disposeErrorManagement();
debugPrint("MyViewModel Disposed");
super.dispose();
}
}Use ViewModelBuilder in your widget to provide and react to your ViewModel.
import 'package:flutter/material.dart';
import 'package:veto/veto.dart';
class MyView extends StatelessWidget {
const MyView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ViewModelBuilder<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 ?? "Veto Example")),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
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 Counter'),
),
const SizedBox(height: 20),
ValueListenableBuilder<bool>(
valueListenable: model.isBusy,
builder: (context, isBusy, _) {
if (isBusy) {
return Column(
children: [
const CircularProgressIndicator(),
if (model.busyMessage != null) ...[
const SizedBox(height: 8),
Text(model.busyMessage!),
]
],
);
}
return const SizedBox.shrink();
},
),
const SizedBox(height: 20),
ValueListenableBuilder<bool>(
valueListenable: model.hasError,
builder: (context, hasError, _) {
if (hasError) {
return Text(
model.errorMessage ?? 'An unknown error occurred.',
style: const TextStyle(color: Colors.red),
);
}
return const SizedBox.shrink();
},
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: model.performFailableOperation,
child: const Text('Perform Failable Operation'),
),
],
),
),
);
},
);
}
}Inside the builder function of ViewModelBuilder, access your ViewModel directly and use ValueListenableBuilder for fine-grained updates.
The BusyManagement mixin provides:
isBusy: AValueListenable<bool>for the busy state.setBusy(bool isBusy, {String? title, String? message}): Call this to update the busy state.
The ErrorManagement mixin provides:
hasError: AValueListenable<bool>for the error state.setError(bool hasError, {String? title, String? message}): Call this to update the error state.
For application-wide loading indicators:
void main() {
BusyService.initialise(
busyMessageDefault: "Please wait...",
busyTypeDefault: BusyType.indicatorBackdrop,
timeoutDurationDefault: const Duration(seconds: 30),
);
runApp(MyApp());
}Use BusyServiceManagement mixin or BusyService.instance() in ViewModels, and listen in your root widget’s builder:
class MyApp extends StatelessWidget { ... }Pass arguments with argumentBuilder and access via arguments in initialise().
Check the /example directory for a complete Flutter application demonstrating Veto’s features.
provider: Used internally byViewModelBuilderfor efficient state propagation.
Contributions are welcome! Please open issues or pull requests on our GitHub repository.
This package is licensed under the BSD 3-Clause License. See the LICENSE file for details.
If you have any questions or need help, feel free to contact us through codaveto.com or open an issue on GitHub.
