Flutter MVSP design pattern
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.
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.
If you want to apply this article first you must install the scoped model package to your project to stay on track, from here.
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(),),); }}
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