Flutter MVSP design pattern

YahYa Madkhali
Oct 22, 2018 · 4 min read
Image for post
Image for post

As the flutter mix IOS and Android into one single code base run on both of them. I decided to mix the MVP design pattern and Scoped Model package to the new one design pattern called MVSP Model View Scoped Presenter.

This new design pattern MVSP will help you a lot to manage the state of the widget under scoped model descendant widget even with StatelessWidget and make your app flexible, maintainable, and very easy to move any widget in the tree of the app.

So, let’s begin with the little diagram that shows the structure of MVSP.

Image for post
Image for post
MVSP Design Pattern

As we see in the diagram, we have the Presenter object and the extended model class from the mother Model class was mixed together.

Time to look more in details about this MVSP.

First, our model class called Person contain a simple attribute.


class Person {
final String name;
final int age;
final String nat;

const Person(this.name, this.age, this.nat);
@override String toString() {
return 'Name: $name age: $age Statue: $nat';
}
Person.fromMap(Map<String, dynamic> map)
: name = '${map['name']['first']} ${map['name']['last']}',
age = map['registered']['age'],nat = map['nat'];}

Second, Mock class data.

import 'dart:async';
import 'package:mvsp/Model/Person.dart';
class MockPerson implements PersonRepository { @override Future<List<Person>> fetchPersonRepo() { return Future.value(mockPersons);
}}
final List<Person> mockPersons =
[Person('Marry', 12, 'Mock'),
Person('Alex', 32, 'Mock'),
Person('YahYa', 43, 'Mock'),
Person('Kind', 65, 'Mock'),
Person('JoJo', 34, 'Mock'),
Person('SubWay', 16, 'Mock'),
Person('Name', 12, 'Mock'),];

Third, Production class to fetch real data from API.

import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:mvsp/Model/Person.dart';
class ProductionPerson implements PersonRepository { final url = 'https://api.randomuser.me/?results=10';@override
Future<List<Person>> fetchPersonRepo() {
return http.get(url).then((response) {
final statusCode = response.statusCode;
final body = response.body;
if (statusCode != 200 || body == null) {
throw Exception('An error while fetch persons data StatusCode $statusCode');
}

final decodeBody = json.decode(body);
final persons = decodeBody['results'] as List;
return persons.map((prn) => Person.fromMap(prn)).toList();
}); }}

Fourthly, Presenter class and Injector class. the Presenter class is responsible for fetch data to the Model class. Where Injector class is responsible for the state of the whole app is in Mock or Production mode.

  • Presenter class
import 'dart:async';
import 'package:mvsp/Model/Person.dart';
import 'package:mvsp/Presenter/Injector.dart';
abstract class ContractFetchPerson {
void fetchPersonList(List<Person> persons);
}
class PresenterPerson { ContractFetchPerson contractPerson;
PersonRepository personRepository;
PresenterPerson(this.contractPerson) {
personRepository = Injector().personsData;
}
Future<void> load() async {
contractPerson.fetchPersonList(await personRepository.fetchPersonRepo());
}
}
  • Injector class
import 'package:mvsp/Model/MockPerson.dart';
import 'package:mvsp/Model/Person.dart';
import 'package:mvsp/Model/ProductionPerson.dart';
enum SystemIsIn { MOCK, PRODUCTION }class Injector {
static final Injector _injector = Injector.internal();
Injector.internal();

factory Injector() => _injector;
static SystemIsIn _systemIsIn;
static void configure(SystemIsIn system) {
_systemIsIn = system; }
PersonRepository get personsData
{
switch (_systemIsIn)
{
case SystemIsIn.MOCK:
return MockPerson();
break;
default:
return ProductionPerson();
}
}
}

Fifthly, The whole article is waiting to talk about this class. this model class deals with the contract of abstract class and the state of any widget created from it. Notice that this class extend the Model class and implement the contract class. So in this way, we can fetch data and manage the state of the widget at the same time efficiency and professional way.

import 'package:mvsp/Model/Person.dart';
import 'package:mvsp/Presenter/ContractPerson.dart';
import 'package:scoped_model/scoped_model.dart';
class ModelScopedPerson extends Model implements ContractFetchPerson{

List<Person> _personsList;
int counter = 0;
List<Person> get personList => _personsList;
PresenterPerson presenter;
bool isLoading;
ModelScopedPerson() {
presenter = PresenterPerson(this);
isLoading = true;
presenter.load();
}
void notify() {
this.notifyListeners();
}
@override
void fetchPersonList(List<Person> persons) {
_personsList = persons;
isLoading = false;
notify(); }}

Sixth, The View class. When we are creating the widget from this class down. This will create Presenter object from it inside contractor. So this will fetch and load data from API immediately. Notice that we were created the Presenter object in the initState() method.

import 'package:flutter/material.dart';
import 'package:mvsp/Model/Person.dart';
import 'package:mvsp/ScopedModelClasses/ModelScopedPerson.dart'; import 'package:scoped_model/scoped_model.dart';
class ViewBody extends StatefulWidget { ViewBody();
@override
ViewBodyState createState() => ViewBodyState();}
class ViewBodyState extends State<ViewBody> {
bool _show = false;
ModelScopedPerson scopedPerson;
@override
void initState() {
scopedPerson = ModelScopedPerson(); // will run Presenter to // fetch and load data immediately.

super.initState();
}
@override
Widget build(BuildContext context) {
/// Scoped Model of this widget start here.
return ScopedModel<ModelScopedPerson>(model: scopedPerson, child: ScopedModelDescendant<ModelScopedPerson>(builder: (BuildContext context, Widget child, model) {
return Column(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[Text( model.counter.toString(), style: TextStyle(fontSize: 20.0, color: Colors.black87), ), model.isLoading?CircularProgressIndicator(): _show ? list([]) // [] to make empty box
: list(model.personList),
Row(mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[FlatButton( onPressed: () { model.counter++;
_show = !_show;
model.notify();
},
child: Text('Show/Hide',
style: TextStyle(color: Colors.blue),)),],)],);},),);}
Widget list(List<Person> x) {
return Expanded(child: ListView(children: x.map((f) =>
ListTile(title: Text(f.name),
trailing: Text(f.nat.toString()),
subtitle: Text('age: ${f.age}'),
)).toList()),);
}}

finally, The main method you can see the Injector.configure in first of its start this method gives two options which are Mock and Production.

import 'package:flutter/material.dart';
import 'package:mvsp/Presenter/Injector.dart';
import 'package:mvsp/View/ViewBody.dart';
void main() {
Injector.configure(SystemIsIn.PRODUCTION);
return runApp(DemoApp());}
class DemoApp extends StatelessWidget {
@override Widget build(BuildContext context) {
return MaterialApp(home: Scaffold(backgroundColor: Colors.white,
appBar: AppBar(elevation: 0.0, backgroundColor: Colors.white, title: Text('MVSP Design Pattern',
style: TextStyle(color: Colors.grey),
),brightness: Brightness.light,
),
body:ViewBody(),),); }}
Image for post
Image for post

Notice that I can change the state of the widget without even use setState() method to re-render the widget.

At the end of this article, this is the Full app repo on GitHub

Thanks for your time

Flutter Community

Articles and Stories from the Flutter Community

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store