An example of implementing a flutter MVVM application using two simple base classes for
StatefulWidget
s and State
respectively.
The main motivation was that I wanted to separate UI code from logic, but am happy to pass down
the ViewModel
via the constructor, i.e. I don't need the Provider
or ScopedModel
or
InheritedWidget
magic which for my usecase add unnecessary complexity.
Basically all I need is to bind the view to changes raised inside the ViewModel
.
Both base classes are implemented inside bound.dart and look as follows.
- used instead of
StatefulWidget
- simply provides a
createViewModel
factory method which needs to be provided when calling it'ssuper
method - the reason we need a factory method is that ViewModels get disposed when the View is
detached and thus need to be created fresh whenever
createState
is invoked on the widget, i.e._MyViewState createState() => _MyViewState(createViewModel());
abstract class BoundWidget<TViewModel extends ChangeNotifier>
extends StatefulWidget {
final TViewModel Function() createViewModel;
BoundWidget(this.createViewModel, {Key key}) : super(key: key);
}
class AddNumberView extends BoundWidget<AddNumberViewModel> {
AddNumberView(createViewModel) : super(createViewModel);
@override
_AddNumberViewState createState() => _AddNumberViewState(createViewModel());
}
- used instead of
State<StatefulWidget>
- takes a
viewModel
which needs to extendChangeNotifier
as input when constructed - simply hooks changes raised by the
viewModel
intosetState
of the view
abstract class BoundState<TState extends StatefulWidget,
TViewModel extends ChangeNotifier> extends State<TState> {
final TViewModel viewModel;
BoundState(this.viewModel) {
viewModel.addListener(() {
this.setState(noop);
});
}
@override
void dispose() {
viewModel.dispose();
super.dispose();
}
}
class _AddNumberViewState
extends BoundState<AddNumberView, AddNumberViewModel> {
_AddNumberViewState(AddNumberViewModel viewModel) : super(viewModel);
@override
Widget build(BuildContext context) {
// [ .. ]
}
}
The sample app just adds/removes and aggregates a list of numbers to demonstrate the interactions of different views.
All state and stream sources are implemented inside NumberService.
All dependencies, including the service, views and viewmodels are registered/resolved via a simple locator. The specific Views are provided the ViewModel factory methods on navigation inside the router.
Only the top ViewModels access that service directly. In one instance an Observable is passed down to a sub ViewModel. Sub ViewModels communicate actions via callbacks to keep things simple. Only the top level ViewModels then invoke a method on the service to incur a change.
All state and direct interaction with that state lives inside that service.
The Views have no notion of the service or RX primitives. Instead they just interact with the ViewModel to which they are bound either by pulling a property to display or invoking a method when the user interacts with the UI.
For now this approach works great for me in this example. I'll try it on a larger app and possibly adjust the approach to further needs. Once I'm sure this covers most needs I'll publish the bound base classes as a package.
Feel free to file issues with questions, concerns and suggestions.