simple_mvvm 1.0.0 simple_mvvm: ^1.0.0 copied to clipboard
A bold and balanced state management library that pairs MVVM structures with the simplicity of InheritedWidget.
Simple MVVM • Documentation • Sample Apps • Pub.dev
A bold and balanced state management library that pairs MVVM structures with the simplicity of InheritedWidget 🐦🍹
Overview #
The Simple MVVM library provides a simple set of widgets to help you pass state data to a subtree.
ViewModel
- A State object that introduces an InheritedWidget to the widget tree.ViewModelBuilder
- A StatefulWidget that you will include in your widgetViewModelProvider
- (Behind the scenes) An InheritedWidget that provides theViewModel
to its children
When building with simple_mvvm, you only need to worry about the ViewModel
and ViewModelBuilder
. The ViewModelProvider
is created for you.
The benefit to this approach is that all children within the subtree can access the ViewModel
- that's just how InheritedWidgets work.
Depending on your app's needs, you can place a ViewModelBuilder
as high up in your app's widget tree as you'd like, making this a convenient way to
pass services, constants, etc to your entire application.
Benefits #
💙 Pure Flutter #
ViewModelProviders
are InheritedWidgets
, meaning you can access them the using methods built into the Flutter framework. Since the ViewModel
is
a
property on the ViewModelProvider
, you can access it using
the dependOnInheritedWidgetOfExactType context method.
This package sets that up for you so can just do this:
HomeViewModel model = HomeViewModel().of(context);
🍋 Easy Model Usage #
The ViewModelProvider
provides your model to its children through its builder property so most of the time you won't need to add code to access
the
model.
return Scaffold(
body: HomeViewModelBuilder(
builder: (context, model) {
return ... // Use the model to render your UI
},
)
);
🔥 No Bloat #
This entire library is 60 lines of dart code with no external dependencies.
Setup #
Step 1:
Create a ViewModel. The ViewModel is a State object that introduces an InheritedWidget to the widget tree. This is where your business logic will live.
class HomeViewModel extends ViewModel<HomeViewModel> {
// For convenience, you can add a static .of_ getter. This is optional
static HomeViewModel of_(BuildContext context) => getModel<HomeViewModel>(context);
// Here is where you will add your business logic and state properties
// Notice that you have access to setState here
ValueNotifier<int> counter = ValueNotifier(0);
void incrementCounter() {
setState(() {
counter.value++;
});
}
}
Step 2:
Create a ViewModelBuilder
. The ViewModelBuilder
is a StatefulWidget that you
will
include in your widget tree. ViewModelBuilder
creates the ViewModel
from above.
class HomeViewModelBuilder extends ViewModelBuilder<HomeViewModel> {
const HomeViewModelBuilder({
super.key,
required super.builder,
});
// Override createState to create the specific ViewModel from above
@override
State<StatefulWidget> createState() => HomeViewModel();
}
Usage #
Once you have your ViewModel
and ViewModelBuilder
, add the ViewModelBuilder
to your widget tree:
return Scaffold(
body: HomeViewModelBuilder(
builder: (context, model) {
return Text('Test')
},
)
);
Now you have several ways to access the ViewModel.
1. Use the provided "model" object: #
return Scaffold(
body: HomeViewModelBuilder(
builder: (context, model) {
return Text(model.title); // Add a title String to your ViewModel
},
)
);
2. Use the getModel<T> helper function: #
Under the hood, the getModel
function uses dependOnInheritedWidgetOfExactType
to get the type you specify in the generic parameter T.
return Scaffold(
body: HomeViewModelBuilder(
builder: (context, model) {
return Text(getModel<HomeViewModel>(context).title); // Add a title String to your ViewModel
},
)
,
);
3. Use the ModelWidget: #
ModelWidget<ScreenTwoViewModel>
(
builder: (context, model) {
return Text(model.counter.value.toString());
},
)
,
4. Use the .of(context) method: #
Each ViewModel
has a built in .of() method. This is useful if you break your widget tree up and need to access the model in a different widget:
return Scaffold(
body: HomeViewModelBuilder(
builder: (context, model) {
return Text(HomeViewModel().of(context).title); // Add a title String to your ViewModel
},
)
,
);
The .of(context) method only works on an instance of your ViewModel
since static members can't reference type parameters of a class.
If you want to save yourself the time it takes to type the extra parenthesis, add a separate method directly in your View Model
(classes can't have
instance and static methods with the same name, hence the ".of_" vs ".of"):
class HomeViewModel extends ViewModel<HomeViewModel> {
// Add this
static HomeViewModel of_(BuildContext context) => getModel<HomeViewModel>(context);
}
To update your UI after a change in the ViewModel
, you have two options.
- Call
setState
to rebuild the entire widget tree inside yourViewModelBuilder
- Use a combination of ValueNotifiers and ValueListenableBuilders to selectively rebuild parts of the UI
You can see examples of each of these approaches in the example directory.
Advanced Usage #
Initialize and Dispose #
Since the ViewModel object extends the State class, you can simply override initState and dispose to run code when the ViewModel is added and removed from the widget tree, respectively:
class HomeViewModel extends ViewModel<HomeViewModel> {
@override
void initState() {
debugPrint('Initialize');
super.initState();
}
@override
void dispose() {
debugPrint('Dispose');
super.dispose();
}
}
Set Loading #
ViewModels include a single ValueNotifier that can be used to mark it as "loading":
ValueNotifier<bool> loading = ValueNotifier(false);
bool get isLoading => loading.value;
void setLoading(bool val) {
setState(() {
loading.value = val;
});
}
For example, you can call model.setLoading(true)
when the ViewModel needs to load asynchronous data. When the data is loaded,
call model.setLoading(false)
. In your UI, you can show a spinner if the loading value is true using the model directly. Whenever the loading value
is changed, the entire UI inside the ViewModelBuilder will be rebuilt.
class HomeView extends StatelessWidget {
const HomeView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return HomeViewModelBuilder(
builder: (context, model) {
return Scaffold(
appBar: AppBar(title: Text(model.title)),
body: Stack(
children: [
Text('Hello World!')
if (model.isLoading) const ColoredBox(color: Colors.black12, child: Center(child: CircularProgressIndicator()))
],
),
);
},
);
}
}
Simple MVVM CLI #
Check out the cotr_cli.
IntelliJ Live Templates #
View #
import 'package:simple_mvvm/simple_mvvm.dart';
import 'package:flutter/material.dart';
import '$snakeName$_model.dart';
class $Name$View extends StatelessWidget {
const $Name$View({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: $Name$ViewModelBuilder(
builder: (context, model) {
return Center(child: Text('$Name$'););
},
),
);
}
}
ViewModel and ViewModelBuilder #
import 'package:simple_mvvm/simple_mvvm.dart';
import 'package:flutter/material.dart';
class $Name$ViewModelBuilder extends ViewModelBuilder<$Name$ViewModel> {
const $Name$ViewModelBuilder({
super.key,
required super.builder,
});
@override
State<StatefulWidget> createState() => $Name$ViewModel();
}
class $Name$ViewModel extends ViewModel<$Name$ViewModel> {
static $Name$ViewModel of_(BuildContext context) => getModel<$Name$ViewModel>(context);
}
You can read more about using variables in Live Templates here.