Application of mvp in flutter


In the process of Android application development, we often use some so-called architecture methods, such as mvp, mvvm, clean, etc.These methods are recommended because they greatly decouple the functional modules of our code, making it easier for our code to expand and maintain in the middle and later stages of the project.

I personally recommend mvp, mainly because it is relatively simple and easy to use. This time I will show you how to use MVP in Flutter to organize the functional modules of a project.For demonstration convenience, I chose a simpler address list to show you.

MVP

First, you need to prepare two well-known classes of mvp: IView and IPrensenter, where IView constrains the behavior of views and IPresenter interacts with IView, providing it with other logical processing besides UI behavior, such as network requests, database queries, and so on.

Here we first create a new project named flutter_mvp using IntelliJ, then create a new mvp.dart file in the lib directory, which is as follows:


abstract class IView<T> {
  setPresenter(T presenter);
}

abstract class IPresenter{
  init();
}

Yes, these two classes are so simple.

data source

First, let's not rush to write UI code, just leave the main.dart file intact.First we define a Contact class to represent each item in the address book, then we define a data warehouse interface, ContactRepository, to get the data, coded as follows:

import 'dart:async';

class Contact {
  final String fullName;

  final String email;

  const Contact({this.fullName,this.email});
}


abstract class ContactRepository{
  Future<List<Contact>> fetch();
}

Contact has two fields, fullName and email.ContactRepository has a fetch method for getting a list of address books.

Now that the ContactRepository interface is defined, write its implementation class, MockContactRepository, and create a new file, contact_data_impl.dart, which reads as follows:

import 'dart:async';
import 'contact_data.dart';
import 'package:flutter/services.dart';
import 'dart:convert';
class MockContactRepository implements ContactRepository{

  @override
  Future<List<Contact>> fetch() {
    return new Future.value(kContacts);
  }
}

const kContacts = const<Contact>[
    const Contact(fullName: "Li bai",email: "libai@live.com"),
    const Contact(fullName: "Cheng yaojin",email: "chengyaojin@live.com"),
    const Contact(fullName: "Mi yue",email: "miyue@live.com"),
    const Contact(fullName: "A ke",email: "ake@live.com"),
    const Contact(fullName: "Lu ban",email: "luban@live.com"),
    const Contact(fullName: "Da qiao",email: "daqiao@live.com"),
    const Contact(fullName: "Hou yi",email: "houyi@live.com"),
    const Contact(fullName: "Liu bei",email: "liubei@live.com"),
    const Contact(fullName: "Wang zhaojun",email: "wangzhaoju@live.com"),
  ];

The function of MockContactRepository is to provide false data for testing earlier.

constraint

Next comes the more important step of writing constraints for address book functions, which are IView and IPresenter.Create a new contract.dart file, which reads as follows:

import 'package:flutter_mvp/mvp.dart';
import 'package:flutter_mvp/contact/data/contact_data.dart';

abstract class Presenter implements IPresenter{
  loadContacts();
}

abstract class View implements IView<Presenter>{
  void onLoadContactsComplete(List<Contact> items);
  void onLoadContactsError();
}

Here we define two constraints for our address book, Presenter and View, where Presenter provides a loadContacts method to load data.View provides the onLoadContactsComplete method for updating the interface; onLoadContactsError is used for error handling of the interface.

Implementation of Presenter

Next, we implement the Presenter interface and create a new file, contact_presenter.dart, which contains the following:

import 'package:flutter_mvp/contact/contract.dart';
import 'package:flutter_mvp/contact/data/contact_data.dart';
import 'package:flutter_mvp/contact/data/contact_data_impl.dart';

class ContactPresenter implements Presenter{

  View _view;

  ContactRepository _repository;

  ContactPresenter(this._view){
    _view.setPresenter(this);
  }
  
  @override
  void loadContacts(){
    assert(_view!= null);

    _repository.fetch().then(
            (contacts){
              _view.onLoadContactsComplete(contacts);
            })
          .catchError((error){
            print(error);
            _view.onLoadContactsError();
          }
    );
  }
  @override
  init() {
    _repository = new MockContactRepository();
  }
}

The Presenter initializes its own _view field in the construction method and calls the setPresenter method of _view to inject the presenter object into it.This binds View and Presenter together.The _repository object is then initialized in the init method.

The focus here is the loadContacts method, which calls the fetch method of the _repository to get the data and the onLoadContactsComplete method of _view to update the UI when the data is obtained.

Implementation of View

Finally, our UI section, where we create a new file, contact_page.dart, which reads as follows:


import 'package:flutter/material.dart';
import 'package:flutter_mvp/contact/data/contact_data.dart';
import 'package:flutter_mvp/contact/contact_presenter.dart';
import 'package:flutter_mvp/contact/contract.dart';
class ContactsPage extends StatelessWidget{

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
        appBar: new AppBar(
          title: new Text("Contacts"),
        ),
        body: new ContactList()
    );
  }
}

class ContactList extends StatefulWidget{
  ContactList({ Key key }) : super(key: key);

  @override
  _ContactListState createState(){
    _ContactListState view = new _ContactListState();
    ContactPresenter presenter = new ContactPresenter(view);
    presenter.init();
    return view ;
  }
}

class _ContactListState extends State<ContactList> implements View {

  List<Contact> contacts = [];

  ContactPresenter _presenter;

  @override
  void initState() {
    super.initState();
    _presenter.loadContacts();
  }

  Widget buildListTile(BuildContext context, Contact contact) {

    return new MergeSemantics(
      child: new ListTile(
        isThreeLine: true,
        dense: false,
        leading:  new ExcludeSemantics(child: new CircleAvatar(child: new Text(contact.fullName.substring(0,1)))) ,
        title: new Text(contact.fullName),
        subtitle: new Text(contact.email),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {

    Widget widget ;

    widget = new ListView.builder(padding: new EdgeInsets.symmetric(vertical: 8.0),
      itemBuilder: (BuildContext context, int index){
          return buildListTile(context,contacts[index]);
      },
    itemCount: contacts.length,
    );
    return widget;
  }
  @override
  void onLoadContactsComplete(List<Contact> items) {
    setState((){
      contacts = items;
      print("  contacts size  ${contacts.length}");
    });
  }

  @override
  void onLoadContactsError() {
  }
  
  @override
  setPresenter(Presenter presenter) {
    _presenter = presenter;
  }
}

This code is a bit long, let's look at it in sections.

The first is the ContactsPage class, which is primarily used to provide AppBar s and bodies on the UI.Where body is ContactList is our address list.

Next, look at the ContactList, whose createState method is as follows:

 @override
  _ContactListState createState(){
    _ContactListState view = new _ContactListState();
    ContactPresenter presenter = new ContactPresenter(view);
    presenter.init();
    return view ;
  }

First, the UI class _ContactListState for the address book is initialized, then the ContactPresenter is initialized and _ContactListState is passed in.Finally, the init method of Presenter was called to initialize Presenter.

Next comes the _ContactListState class, from which the address list is built.The UI-related code doesn't say much, but here's the initState method, in which Presenter's loadContacts method is called to load data.The onLoadContactsComplete method of _ContactListState is called to update the UI when Presenter has loaded the data.

The final results are as follows:


Use Real Data

Above we use the fake data provided by MockContactRepository, then we define an HttpContactRepository to load data from the network, add the HttpContactRepository class in contact_data_impl,


const String kContactsUrl = "http://o6p4e1uhv.bkt.clouddn.com/contacts.json";

class HttpContactRepository implements ContactRepository{

  @override
  Future<List<Contact>> fetch() async{
    var httpClient = createHttpClient();
    var response = await httpClient.get(kContactsUrl);
    var body = response.body;
    List<Map> contacts = JSON.decode(body)['contacts'];
    return contacts.map((contact){
      return new Contact(fullName:  contact['fullname'],email:  contact['email']);
    }).toList();
  }
}

In order to switch between HttpContactRepository and MackContactRepository, two additional classes, RepositoryType and Injector, are added.

enum RepositoryType{
  mock,http
}

class Injector{

  ContactRepository getContactRepository(RepositoryType type){
    switch(type){
      case RepositoryType.mock:
        return new MockContactRepository();
      default:
        return new HttpContactRepository();
    }
  }

}

Injector manages external dependency on ContactRepository.

The final contact_data_impl file contains the following:

import 'dart:async';
import 'contact_data.dart';
import 'package:flutter/services.dart';
import 'dart:convert';
class MockContactRepository implements ContactRepository{

  @override
  Future<List<Contact>> fetch() {
    return new Future.value(kContacts);
  }
}

class HttpContactRepository implements ContactRepository{

  @override
  Future<List<Contact>> fetch() async{
    var httpClient = createHttpClient();
    var response = await httpClient.get(kContactsUrl);
    var body = response.body;
    List<Map> contacts = JSON.decode(body)['contacts'];
    return contacts.map((contact){
      return new Contact(fullName:  contact['fullname'],email:  contact['email']);
    }).toList();
  }
}

enum RepositoryType{
  mock,http
}

class Injector{

  ContactRepository getContactRepository(RepositoryType type){
    switch(type){
      case RepositoryType.mock:
        return new MockContactRepository();
      default:
        return new HttpContactRepository();
    }
  }

}

const String kContactsUrl = "http://o6p4e1uhv.bkt.clouddn.com/contacts.json";

const kContacts = const<Contact>[
    const Contact(fullName: "Li bai",email: "libai@live.com"),
    const Contact(fullName: "Cheng yaojin",email: "chengyaojin@live.com"),
    const Contact(fullName: "Mi yue",email: "miyue@live.com"),
    const Contact(fullName: "A ke",email: "ake@live.com"),
    const Contact(fullName: "Lu ban",email: "luban@live.com"),
    const Contact(fullName: "Da qiao",email: "daqiao@live.com"),
    const Contact(fullName: "Hou yi",email: "houyi@live.com"),
    const Contact(fullName: "Liu bei",email: "liubei@live.com"),
    const Contact(fullName: "Wang zhaojun",email: "wangzhaoju@live.com"),
  ];

The last thing to change is the init method of the ContactPresenter class.

  @override
  init() {
    _repository = new Injector().getContactRepository(RepositoryType.mock);
  }

This makes it easy to switch between real and test data.

summary

Whether MVP is simpler or not depends on the definition and implementation of View and Presenter.In addition, if you are not familiar with mvp, you can find more information on the Internet.

If you need the above code, you can https://github.com/flutter-dev/flutter-mvp Download.

As a final ad, our Flutter Chinese Developer Forum is online. If you are interested in Flutter, you can go to it flutter-dev.cn/bbs or flutter-dev.com/bbs Discuss and learn with you.

Keywords: JSON network Android Database

Added by masteroleary on Wed, 22 May 2019 20:26:05 +0300