The MVP architecture pattern in Flutter with simple demo

Liem Vo
Liem Vo
Aug 28, 2018 · 6 min read

The MVP architecture pattern is a derivation from the MVC pattern wherein the Controller is replaced by the Presenter. The MVP divides an application into three layers: Model, View, and Presenter.

Credit: https://www.techyourchance.com/

According Wikipedia:

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

So in the MVP pattern, the presenter is the middle mediator handling the model and updating the view. In MVP View and Presenter are completely decoupled and communicate each other by the interfaces. Because of decoupling of MVP that is easy to mock data for unit testing.

Here is the UI of Application:

BMI demo UI

Basically, in BMI example I define three layers:

MVP architecture pattern layers

Model package has a model class that will update the model according to the user action via the presenter then change the value.

class BMIViewModel {
double _bmi = 0.0;
UnitType _unitType = UnitType.FeetPound;

double height;
double weight;

double get bmi => _bmi;
set bmi(double outBMI){
_bmi = outBMI;
}

UnitType get unitType => _unitType;
set unitType(UnitType setValue){
_unitType = setValue;
}

int get value => _unitType == UnitType.FeetPound?0 : 1;
set value(int value){
_unitType = value == 0? UnitType.FeetPound: UnitType.KilogamMetter;
}

String get heightMessage => _unitType == UnitType.FeetPound? "Height in feets" : "Height in metters";
String get weightMessage => _unitType == UnitType.FeetPound? "Weight in pounds" : "Weight in kilogams";
String get bmiMessage => determineBMIMessage(_bmi);
String get bmiInString => bmi.toStringAsFixed(2);
String get heightInString => height != null ? height.toString():'';
String get weightInString => weight != null ? weight.toString():'';

BMIViewModel();
}

Presenter package contains the interface and presenter class. BMIPresenter is an interface including 3 methods will handle the logic business from BMIView and update Model. Class BasicBMIPresenter which is an implementation of presenter interface is holding a BMIView interface and a BMIViewModel.

class BasicBMIPresenter implements BMIPresenter{
BMIViewModel _viewModel;
BMIView _view;

BasicBMIPresenter() {
this._viewModel = BMIViewModel();
_loadUnit();
}

void _loadUnit() async{
_viewModel.value = await loadValue();
_view.updateUnit(_viewModel.value, _viewModel.heightMessage, _viewModel.weightMessage);

}

@override
set bmiView(BMIView value) {
_view = value;
_view.updateUnit(_viewModel.value, _viewModel.heightMessage, _viewModel.weightMessage);
}

@override
void onCalculateClicked(String weightString, String heightString) {
var height = 0.0;
var weight = 0.0;
try {
height = double.parse(heightString);
} catch (e){

}
try {
weight = double.parse(weightString);
} catch (e) {

}
_viewModel.height = height;
_viewModel.weight = weight;
_viewModel.bmi = calculator(height, weight, _viewModel.unitType);
_view.updateBmiValue(_viewModel.bmiInString, _viewModel.bmiMessage);
}

@override
void onOptionChanged(int value, {String weightString, String heightString}) {


if (value != _viewModel.value) {
_viewModel.value = value;
saveValue(_viewModel.value);
var height;
var weight;
if (!isEmptyString(heightString)) {
try {
height = double.parse(heightString);
} catch (e) {

}
}
if (!isEmptyString(weightString)) {
try {
weight = double.parse(weightString);
} catch (e) {

}
}

if (_viewModel.unitType == UnitType.FeetPound) {
if (weight != null) _viewModel.weight = weight * 2.2046226218;
if (height != null) _viewModel.height = height * 3.28084;
} else {
if (weight != null) _viewModel.weight = weight / 2.2046226218;
if (height != null) _viewModel.height = height / 3.28084;
}

_view.updateUnit(_viewModel.value, _viewModel.heightMessage, _viewModel.weightMessage);
_view.updateHeight(height: _viewModel.heightInString);
_view.updateWeight(weight: _viewModel.weightInString);
}
}
}
  • View layer includes an interface BMIView with a method updateView that will be implemented in _HomePageState class. The View has a BMIPresenter interface that will handle the logic of the View such as change the unit or calculate the BMI value. When the user taps on these actions the presenter interface will invoke these functions that are implemented in the BasicBMIPresenter and update the model value. After that model will notify the change via Presenter.

View interface:

class BMIView {
void updateBmiValue(String bmiValue, String message){}
void updateWeight({String weight}){}
void updateHeight({String height}){}
void updateUnit(int value, String heightMessage, String weightMessage){}
}

View class:

class HomePage extends StatefulWidget {
final BMIPresenter presenter;

HomePage(this.presenter, {Key key, this.title}) : super(key: key);
final String title;

@override
_HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> implements BMIView {
//BMIViewModel _viewModel;
var _ageController = new TextEditingController();
var _heightController = new TextEditingController();
var _weightController = new TextEditingController();
var _message = '';
var _bmiString = '';
var _value = 0;
var _heightMessage = '';
var _weightMessage = '';

@override
void initState() {
super.initState();
this.widget.presenter.bmiView = this;

}

void handleRadioValueChanged(int value) {
this.widget.presenter.onOptionChanged(value, heightString: _heightController.text, weightString: _weightController.text );
}

void _calculator() {
this.widget.presenter.onCalculateClicked(_weightController.text, _heightController.text);
}

@override
void updateBmiValue(String bmiValue, String bmiMessage){
setState(() {
_bmiString = bmiValue;
_message = bmiMessage;
});
}
@override
void updateWeight({String weight}){
setState(() {
_weightController.text = weight != null?weight:'';
});
}
@override
void updateHeight({String height}){
setState(() {
_heightController.text = height != null?height:'';
});
}
@override
void updateUnit(int value, String heightMessage, String weightMessage){
setState(() {
_value = value;
_heightMessage = heightMessage;
_weightMessage = weightMessage;
});
}

@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('BMI'),
centerTitle: true,
backgroundColor: Colors.pinkAccent.shade400,
),
backgroundColor: Colors.white,
body: new Container(
child: new ListView(
children: <Widget>[
new Image.asset('images/bmilogo.png',

width: 100.0,
height: 100.0,),
new Padding(padding: new EdgeInsets.all(5.0)),
new Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Radio<int>(
activeColor: Colors.blueAccent,
value: 0, groupValue: _value, onChanged: handleRadioValueChanged,
),
new Text(
'Feet & pounds',
style: new TextStyle(color: Colors.blueAccent),
),
new Radio<int>(
activeColor: Colors.blueAccent,
value: 1, groupValue: _value, onChanged: handleRadioValueChanged,
),
new Text(
'Meters & kilograms',
style: new TextStyle(color: Colors.blueAccent),
),
],
),
new Padding(padding: new EdgeInsets.all(5.0)),
new Container(
width: 380.0,
height: 240.0,
color: Colors.grey,
child: new Column(
children: <Widget>[
new TextFormField(
controller: _ageController,
keyboardType: TextInputType.number,
decoration: new InputDecoration(
labelText: 'Age',
hintText: 'Age',
icon: new Icon(Icons.person_outline),
fillColor: Colors.white,
),
),
new TextFormField(
controller: _heightController,
keyboardType: TextInputType.number,
decoration: new InputDecoration(
labelText: _heightMessage,
hintText: _heightMessage,
icon: new Icon(Icons.assessment),
fillColor: Colors.white,
),
),
new TextFormField(
controller: _weightController,
keyboardType: TextInputType.number,
decoration: new InputDecoration(
labelText: _weightMessage,
hintText: _weightMessage,
icon: new Icon(Icons.menu),
fillColor: Colors.white
),
),
new Padding(padding: new EdgeInsets.all(4.5)),
new Center(
child: new RaisedButton(
onPressed: _calculator,
color: Colors.pinkAccent,
child: new Text(
'Calculate',
style: new TextStyle(fontSize: 16.9),
),
textColor: Colors.white70,
),
)
],
),

),
new Padding(padding: new EdgeInsets.all(5.0)),
new Column(
children: <Widget>[
new Center(
child: new Text(
'Your BMI: ${_bmiString}',
style: new TextStyle(
color: Colors.blue,
fontSize: 24.0,
fontWeight: FontWeight.w700,
fontStyle: FontStyle.italic
),
),
),
new Padding(padding: new EdgeInsets.all(2.0)),
new Center(
child: new Text(
'${_message}',
style: new TextStyle(
color: Colors.pinkAccent,
fontSize: 24.0,
fontWeight: FontWeight.w600
),
),
)
],
)
],
),
)
);
}

}

Other resources like utils or main.dart are handling the utility functions, enum, and initial the app.
This is the first post in English so it can contain a lot of mistakes. Welcome, all comment and the correction.

You can checkout the source code here:
https://github.com/liemvo/Flutter_bmi


The Flutter Pub is a medium publication to bring you the latest and amazing resources such as articles, videos, codes, podcasts etc. about this great technology to teach you how to build beautiful apps with it. You can find us on Facebook, Twitter, and Medium or learn more about us here. We’d love to connect! And if you are a writer interested in writing for us, then you can do so through these guidelines.

FlutterPub

The Pub(lication) for all about the Flutter and its magic

Liem Vo

Written by

Liem Vo

Senior mobile Developer (https://github.com/liemvo)

FlutterPub

The Pub(lication) for all about the Flutter and its magic

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade