code_on_the_rocks 0.0.9 code_on_the_rocks: ^0.0.9 copied to clipboard
A bold and balanced state management library that pairs MVVM structures with the simplicity of InheritedWidget.
Code on the Rocks • Documentation • Sample Apps • Pub.dev
A bold and balanced state management library that pairs MVVM structures with the simplicity of InheritedWidget 🐦🍹
Inspiration #
Over the years I've become a big fan of several different "state-management" solutions:
- InheritedWidgets are powerful widgets built into the Flutter Framework
- Stacked is an opinionated MVVM library that uses ViewModels, ViewModelBuilders, and ViewModelWidgets
- get_it lets you easily access services from anywhere in your app
- get_it_mixin lets you bind views to services stored in GetIt
This package is an attempt to combine the best parts of these solutions into a single package that is easy to use and understand. The main benefits are listed here:
💙 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 46 lines of dart code with no external dependencies.
Overview #
The Code on the Rocks library provides a simple, reusable set of widgets to help you pass state data from a ViewModel
to a View.
The ViewModelBuilder
will be added directly to your widget tree and its state (the ViewModel
) will be passed to its subtree by
the ViewModelProvider
/InheritedWidget.
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.
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 .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 your ViewModelBuilder - 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()))
],
),
);
},
);
}
}
IntelliJ Live Templates #
View #
import 'package:code_on_the_rocks/code_on_the_rocks.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:code_on_the_rocks/code_on_the_rocks.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.