Flutter MVP Architecture
What is the MVP?
MVP stands for Model View Presenter.
The model is an interface defining the data to be displayed or otherwise acted upon in the user interface.
The view is a passive interface that displays data (the model) and routes user commands (events) to the presenter to act upon that data.
The presenter acts upon the model and the view. It retrieves data from repositories (the model), and formats it for display in the view
Communication Flow
We will see from this Flow Diagram how the model view and presenter are connected to make our code more readable and easier to test.
MVP consists of model view and presenter and they are bonded in such a way that the View in the topmost layer is responsible for showing the data to the user and taking data from the user.
The presenter contains the UI business logic for the View. All invocations from the View delegate directly to the Presenter.
The view is the topmost layer of the architecture, interacts with the user and takes the input which is being passed to the presenter, and taking the data from the model it sends the data back to the View to present it to the user.
- The view is more loosely coupled to the model. The presenter is responsible for binding the model to the view.
- Easier to unit test because interaction with the view is through an interface.
- Usually view to presenter map one to one. Complex views may have multi presenters.
Usage of this library
Let’s say we have our loved counter sample:
class CounterExample extends StatefulWidget { const CounterExample({ Key? key,}) : super(key: key);@override_MyHomePageState createState() => _MyHomePageState();}class _MyHomePageState extends State<CounterExample>
implements Counter {final BasicCounterPresenter presenter = BasicCounterPresenter();CounterModel _viewModel = CounterModel(0);@overridevoid initState() {super.initState(); presenter.counterView = this;}@overridevoid refreshCounter(CounterModel viewModel) {setState(() { _viewModel = viewModel;});}@overrideWidget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Mvp demo'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ const Padding(
padding: EdgeInsets.only(bottom: 30.0), child: Text("Click buttons to add and substract.",), ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ FloatingActionButton( onPressed: () { presenter.decrementCounter(); }, child: const Icon(Icons.remove), ), Text("${_viewModel.counter}", ), FloatingActionButton( onPressed: () { presenter.incrementCounter(); }, child: const Icon(Icons.add), ), ], ), ], ), ),);}}
In this simple app our model is the actual counter value, let’s extract the counter var into a dedicated model class:
class MainModel {
MainModel({this.counter = 0});
int counter;
}
We need a presenter that will receive the user input (click on the increment counter button), compute the new counter value, and then apply this to the view:
class BasicCounterPresenter { final CounterModel _counterViewModel = CounterModel(0); Counter _counterView = Counter();void incrementCounter() { _counterViewModel.counter++; _counterView.refreshCounter(_counterViewModel);}void decrementCounter() { _counterViewModel.counter--; _counterView.refreshCounter(_counterViewModel);}set counterView(Counter value) { _counterView = value; _counterView.refreshCounter(_counterViewModel); }}
Finally, we release the view from the task of computing the new counter value, only receive user input and render the current view model:
class Counter {void refreshCounter(CounterModel viewModel) {}}