As a reminder MVVM stands for Model-View-ViewModel and is a pattern that is found in many client side applications which makes Flutter an ideal platform. For any of you who are unfamiliar with the pattern you can find information about it here: MVVM Pattern.
To create the framework I wanted to achieve the following design goals to make an enterprise ready framework:
- The only logic in a view (widget) should be presentation logic, viewmodels represent the app structure and Models are business entities. These concepts should be kept separate and represent the app layers.
- Views can know about viewmodels or models. Viewmodels can know about models but nothing about what views may be used to display them. Models similarly will know nothing about the viewmodels or views that may use them.
- Viewmodels and models will not know about anything on a 'higher' layer, but instead raise events when pertinent information changes to anything that may be interested.
- Views can use a concept of data binding to bind to viewmodels and views and have automatic and potentially bidirectional updates. That is to say, a binding between a view and a viewmodel's property will update the UI when the viewmodel's property changes and the view model's property will be updated when the value in the view changes.
- There needs to be a way to reformat information in a viewmodel or model in a way that that is needed by the view (value conversion).
- The framework should have a concept of inversion of control / dependency injection.
- Navigation is an app structure concept and should be controlled by the viewmodel. Since viewmodels shouldn't know about the Views that use them, navigation moves from viewmodel to viewmodel. It is the job of the framework to decide what view to used for a viewmodel.
- Much of the default functionality of the framework should be overridable.
The result of this is a framework I call fmvvm (Flutter MVVM). I tried to make it pretty easy to implement. Here are some quick examples on using it:
Bootstrapping
Once the fmvvm package is added to your app, bootstrapping is just as easy as extending from FmvvmApp and overriding a few methods:
1: void main() {
2: runApp(MyApp());
3: }
4: class MyApp extends FmvvmApp {
5: @override
6: void registerComponents(ComponentResolver componentResolver) {
7: super.registerComponents(componentResolver);
8: // Add component registrations (Inversion of control)
9: }
10: @override
11: String getInitialRoute() {
12: // return initial route name
13: }
14: @override
15: Route getRoutes(RouteSettings settings) {
16: // return a route object that handles the requested route.
17: }
18: @override
19: String getTitle() {
20: // return the app title.
21: }
22: @override
23: ThemeData getTheme() {
24: // optionally override to return a theme for your app.
25: }
26: }
To start fmvvm we've extended from the FmvvmApp object and a few overrides later, we are ready to go.
Creating a model
Models can really inherit from anything, but they should inherit from BindableBase if you want them to take advantage of data binding. For example:
1: import 'package:fmvvm/fmvvm.dart';
2: class Event extends BindableBase {
3: static PropertyInfo descriptionProperty = PropertyInfo('description', String, '');
4: String get description => getValue(descriptionProperty);
5: set description(String value) => setValue(descriptionProperty, value);
6: }
The static PropertyInfo class combined with a getter and setter is what makes two way data binding possible. If the description property changes, an event will be raised so a view that is bound to this object can be updated automatically.
Creating a viewmodel
Creating a viewmodel is similar. ViewModels are items that can be navigated to and are what normally backs a view. Models can also back widgets that are part of a larger view backed by a viewmodel.
1: class EventsViewModel extends ViewModel {
2: final IGalaEventService _galaEventService;
3: EventsViewModel(this._galaEventService)
4: events = _galaEventService.getEvents();
5: }
6: @override
7: void init(Object parameter) {
8: events = _galaEventService.getEvents();
9: }
10: static PropertyInfo eventsProperty = PropertyInfo('events', NotificationList);
11: NotificationList<Event> get events => getValue(eventsProperty);
12: set events(NotificationList<Event> value) => setValue(eventsProperty, value);
13: }
There are a couple of things to notice here:
* We have constructor injection! An instance of the GalaEventService is being injected into the constructor of the viewmodel.
* We have a lifecycle event, init(). This allows parameters to be passed between view models. It is called after the viewmodel is created but before it is displayed. This is intended to do any fetching of data.
* We have something called the NotificationList. This class derives from the normal List but raises events when items are added or removed from the list, this can be used for data binding.
This is a small taste of what is in the fmvvm box. It is intended to create an enterprise app structure that can be used in your applications.
For more information on how to use the framework, including setting up databinding, inversion of control and navigation see the package site: fmvvm.
If you have ideas on ways to improve fmvvm I'd love to hear them. Of course, push requests are always welcomed.