/flutter_mvvm

An example of implementing a flutter MVVM application using two simple base classes for StatefulWidgets and State respectively.

Primary LanguageDart

flutter_mvvm

An example of implementing a flutter MVVM application using two simple base classes for StatefulWidgets 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.

BoundWidget

  • used instead of StatefulWidget
  • simply provides a createViewModel factory method which needs to be provided when calling it's super 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);
}

Example Use

class AddNumberView extends BoundWidget<AddNumberViewModel> {
  AddNumberView(createViewModel) : super(createViewModel);

  @override
  _AddNumberViewState createState() => _AddNumberViewState(createViewModel());
}

add-number-view.dart

BoundState

  • used instead of State<StatefulWidget>
  • takes a viewModel which needs to extend ChangeNotifier as input when constructed
  • simply hooks changes raised by the viewModel into setState 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();
  }
}

Example Use

class _AddNumberViewState
    extends BoundState<AddNumberView, AddNumberViewModel> {
  _AddNumberViewState(AddNumberViewModel viewModel) : super(viewModel);

  @override
  Widget build(BuildContext context) {
    // [ .. ]
  }
}

add-number-view.dart

Sample App

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.

Summary

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.