mvp 在 flutter 中的应用

在 Android 应用程序开发过程中,我们经常会用到一些所谓的架构方法,如:mvp,mvvm,clean等。之所以这些方法会被推崇是因为他们可以大大的解耦我们的代码的功能模块,让我们的代码在项目中后期更容易扩展和维护。

我个人比较推荐 mvp,主要是因为其相对比较简单且易上手,这次我将给大家介绍如何在 Flutter 中使用 mvp 来组织项目的功能模块。为了演示方便,我选择了一个比较简单的通讯录列表来为大家做演示。

MVP

首先需要准备 mvp 鼎鼎有名的两个类:IView和IPrensenter,其中 IView 用于约束视图的行为,IPresenter 则用于与 IView 进行交互,为其提供除了 UI 行为的其他逻辑处理,如网络请求,数据库查询等操作。

这里我们首先使用 IntelliJ 新建一个名为 flutter_mvp 的项目,接着在 lib 目录下新建 mvp.dart 文件,文件内容如下:


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

abstract class IPresenter{
  init();
}

对,这两个类就是如此简单。

数据源

首先我们不急着写 UI 代码,先保持 main.dart 文件不变。我们首先要定义一个 Contact 类,用于表示通讯录中的每一项,接着还要定义一个数据仓库接口 ContactRepository ,用于获取数据,代码如下:

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 有两个字段 fullName 和 email 。ContactRepository 有一个 fetch 方法,用于获取通讯录列表。

既然定义了 ContactRepository 接口,接下来编写它的实现类 MockContactRepository ,新建文件 contact_data_impl.dart ,其内容如下:

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"),
  ];

MockContactRepository 的功能就是在前期提供测试的假数据。

约束

接着是比较重要的环节,为通讯录功能编写约束,约束的内容为 IView 和 IPresenter。新建 contract.dart 文件,内容如下:

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();
}

这里我们给我们的通讯录定义了属于自己的两个约束 Presenter 和 View,其中 Presenter 提供一个 loadContacts 方法,用于加载数据。View 提供了 onLoadContactsComplete 方法,用于更新界面;onLoadContactsError 用于界面的错误处理。

Presenter 的实现

接下来我们首先实现 Presenter 接口,新建文件 contact_presenter.dart文件,文件内容如下:

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();
  }
}

该 Presenter 在构造方法中初始化自己的 _view 字段,并且调用 _view 的 setPresenter 方法,为其注入了 presenter 对象。这样一来 View 和 Presenter 两者就绑定到了一起。接着在 init 方法中初始化了 _repository 对象。

这里的重点是 loadContacts 方法,它会调用 _repository 的 fetch 方法来获取数据,当拿到数据后调用 _view 的 onLoadContactsComplete 方法来更新 UI。

View 的实现

最后就是我们的 UI 部分了,这里新建文件 contact_page.dart ,其内容如下:


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;
  }
}

这段代码有些长,我们分段来看。

首先是类 ContactsPage ,它主要用于提供 UI 上的 AppBar 和 body。其中 body 为 ContactList 就是我们的通讯录列表。

接着看 ContactList ,其 createState 方法如下:

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

首先是初始化了通讯录的 UI 类 _ContactListState,接着初始化了 ContactPresenter ,并将 _ContactListState 传入其中。最后调用了 Presenter 的 init 方法来初始化 Presenter。

接下来就是 _ContactListState 类了,通讯录列表就是由它构建的。UI 相关代码不多说,这里主要看 initState 方法,在其中调用了 Presenter 的 loadContacts 方法来加载数据。当 Presenter 加载完数据后会调用 _ContactListState 的 onLoadContactsComplete 方法来更新 UI 。

最后运行结果如下:

使用真是数据

在上面我们使用的是 MockContactRepository 提供的假数据,接着我们定义一个 HttpContactRepository 来从网络上加载数据,在 contact_data_impl 添加 HttpContactRepository 类,


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();
  }
}

为了 HttpContactRepository 和 MockContactRepository 切换翻遍,另外增加 RepositoryType 和 Injector 两个类,

enum RepositoryType{
  mock,http
}

class Injector{

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

}

其中 Injector 用于管理外界对 ContactRepository 的依赖。

最终 contact_data_impl 文件内容如下:

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"),
  ];

最后需要改动的地方是 ContactPresenter 类的 init 方法,

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

这样就能方便对真是数据和测试数据做切换了。

总结

看到这,是不是觉得 mvp 还是比较简单的,其关键就是对 View 和Presenter 的定义和实现。另外如果对 mvp 还是不很熟悉的可以多在网上找些资料。

如果需要上述代码,可以在https://github.com/flutter-dev/flutter-mvp 下载。

最后做一下广告,我们的 Flutter 中文开发者论坛已经上线了,如果你对 Flutter 感兴趣的话可以前往 flutter-dev.cn/bbsflutter-dev.com/bbs 与大家一起讨论和学习 。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容