mvp 在 flutter 中的應(yīng)用

在 Android 應(yīng)用程序開發(fā)過程中摔吏,我們經(jīng)常會(huì)用到一些所謂的架構(gòu)方法愉镰,如:mvp,mvvm浮定,clean等相满。之所以這些方法會(huì)被推崇是因?yàn)樗麄兛梢源蟠蟮慕怦钗覀兊拇a的功能模塊层亿,讓我們的代碼在項(xiàng)目中后期更容易擴(kuò)展和維護(hù)。

我個(gè)人比較推薦 mvp立美,主要是因?yàn)槠湎鄬?duì)比較簡(jiǎn)單且易上手匿又,這次我將給大家介紹如何在 Flutter 中使用 mvp 來組織項(xiàng)目的功能模塊。為了演示方便建蹄,我選擇了一個(gè)比較簡(jiǎn)單的通訊錄列表來為大家做演示碌更。

MVP

首先需要準(zhǔn)備 mvp 鼎鼎有名的兩個(gè)類:IView和IPrensenter,其中 IView 用于約束視圖的行為洞慎,IPresenter 則用于與 IView 進(jìn)行交互痛单,為其提供除了 UI 行為的其他邏輯處理,如網(wǎng)絡(luò)請(qǐng)求劲腿,數(shù)據(jù)庫查詢等操作旭绒。

這里我們首先使用 IntelliJ 新建一個(gè)名為 flutter_mvp 的項(xiàng)目,接著在 lib 目錄下新建 mvp.dart 文件焦人,文件內(nèi)容如下:


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

abstract class IPresenter{
  init();
}

對(duì)快压,這兩個(gè)類就是如此簡(jiǎn)單。

數(shù)據(jù)源

首先我們不急著寫 UI 代碼垃瞧,先保持 main.dart 文件不變蔫劣。我們首先要定義一個(gè) Contact 類,用于表示通訊錄中的每一項(xiàng)个从,接著還要定義一個(gè)數(shù)據(jù)倉(cāng)庫接口 ContactRepository 脉幢,用于獲取數(shù)據(jù)雌桑,代碼如下:

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 有兩個(gè)字段 fullName 和 email 她紫。ContactRepository 有一個(gè) fetch 方法,用于獲取通訊錄列表邑蒋。

既然定義了 ContactRepository 接口奕污,接下來編寫它的實(shí)現(xiàn)類 MockContactRepository 萎羔,新建文件 contact_data_impl.dart ,其內(nèi)容如下:

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 的功能就是在前期提供測(cè)試的假數(shù)據(jù)碳默。

約束

接著是比較重要的環(huán)節(jié)贾陷,為通訊錄功能編寫約束,約束的內(nèi)容為 IView 和 IPresenter嘱根。新建 contract.dart 文件髓废,內(nèi)容如下:

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

這里我們給我們的通訊錄定義了屬于自己的兩個(gè)約束 Presenter 和 View,其中 Presenter 提供一個(gè) loadContacts 方法该抒,用于加載數(shù)據(jù)慌洪。View 提供了 onLoadContactsComplete 方法,用于更新界面;onLoadContactsError 用于界面的錯(cuò)誤處理冈爹。

Presenter 的實(shí)現(xiàn)

接下來我們首先實(shí)現(xiàn) Presenter 接口涌攻,新建文件 contact_presenter.dart文件,文件內(nèi)容如下:

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 在構(gòu)造方法中初始化自己的 _view 字段频伤,并且調(diào)用 _view 的 setPresenter 方法癣漆,為其注入了 presenter 對(duì)象。這樣一來 View 和 Presenter 兩者就綁定到了一起剂买。接著在 init 方法中初始化了 _repository 對(duì)象惠爽。

這里的重點(diǎn)是 loadContacts 方法,它會(huì)調(diào)用 _repository 的 fetch 方法來獲取數(shù)據(jù)瞬哼,當(dāng)拿到數(shù)據(jù)后調(diào)用 _view 的 onLoadContactsComplete 方法來更新 UI婚肆。

View 的實(shí)現(xiàn)

最后就是我們的 UI 部分了,這里新建文件 contact_page.dart 坐慰,其內(nèi)容如下:


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

這段代碼有些長(zhǎng)较性,我們分段來看。

首先是類 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 傳入其中秸抚。最后調(diào)用了 Presenter 的 init 方法來初始化 Presenter速和。

接下來就是 _ContactListState 類了,通訊錄列表就是由它構(gòu)建的剥汤。UI 相關(guān)代碼不多說颠放,這里主要看 initState 方法,在其中調(diào)用了 Presenter 的 loadContacts 方法來加載數(shù)據(jù)吭敢。當(dāng) Presenter 加載完數(shù)據(jù)后會(huì)調(diào)用 _ContactListState 的 onLoadContactsComplete 方法來更新 UI 碰凶。

最后運(yùn)行結(jié)果如下:

使用真是數(shù)據(jù)

在上面我們使用的是 MockContactRepository 提供的假數(shù)據(jù),接著我們定義一個(gè) HttpContactRepository 來從網(wǎng)絡(luò)上加載數(shù)據(jù)鹿驼,在 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 兩個(gè)類蠢沿,

enum RepositoryType{
  mock,http
}

class Injector{

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

}

其中 Injector 用于管理外界對(duì) ContactRepository 的依賴伸头。

最終 contact_data_impl 文件內(nèi)容如下:

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

最后需要改動(dòng)的地方是 ContactPresenter 類的 init 方法,

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

這樣就能方便對(duì)真是數(shù)據(jù)和測(cè)試數(shù)據(jù)做切換了舷蟀。

總結(jié)

看到這,是不是覺得 mvp 還是比較簡(jiǎn)單的,其關(guān)鍵就是對(duì) View 和Presenter 的定義和實(shí)現(xiàn)野宜。另外如果對(duì) mvp 還是不很熟悉的可以多在網(wǎng)上找些資料扫步。

如果需要上述代碼,可以在https://github.com/flutter-dev/flutter-mvp 下載匈子。

最后做一下廣告河胎,我們的 Flutter 中文開發(fā)者論壇已經(jīng)上線了,如果你對(duì) Flutter 感興趣的話可以前往 flutter-dev.cn/bbsflutter-dev.com/bbs 與大家一起討論和學(xué)習(xí) 虎敦。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末游岳,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子其徙,更是在濱河造成了極大的恐慌胚迫,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,490評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件唾那,死亡現(xiàn)場(chǎng)離奇詭異访锻,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)闹获,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門期犬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人避诽,你說我怎么就攤上這事龟虎。” “怎么了沙庐?”我有些...
    開封第一講書人閱讀 165,830評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵遣总,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我轨功,道長(zhǎng)旭斥,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,957評(píng)論 1 295
  • 正文 為了忘掉前任古涧,我火速辦了婚禮垂券,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘羡滑。我一直安慰自己菇爪,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評(píng)論 6 393
  • 文/花漫 我一把揭開白布柒昏。 她就那樣靜靜地躺著凳宙,像睡著了一般。 火紅的嫁衣襯著肌膚如雪职祷。 梳的紋絲不亂的頭發(fā)上氏涩,一...
    開封第一講書人閱讀 51,754評(píng)論 1 307
  • 那天届囚,我揣著相機(jī)與錄音,去河邊找鬼是尖。 笑死意系,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的饺汹。 我是一名探鬼主播蛔添,決...
    沈念sama閱讀 40,464評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼兜辞!你這毒婦竟也來了迎瞧?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤逸吵,失蹤者是張志新(化名)和其女友劉穎凶硅,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體胁塞,經(jīng)...
    沈念sama閱讀 45,847評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡咏尝,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評(píng)論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了啸罢。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片编检。...
    茶點(diǎn)故事閱讀 40,137評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖扰才,靈堂內(nèi)的尸體忽然破棺而出允懂,到底是詐尸還是另有隱情,我是刑警寧澤衩匣,帶...
    沈念sama閱讀 35,819評(píng)論 5 346
  • 正文 年R本政府宣布蕾总,位于F島的核電站,受9級(jí)特大地震影響琅捏,放射性物質(zhì)發(fā)生泄漏生百。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評(píng)論 3 331
  • 文/蒙蒙 一柄延、第九天 我趴在偏房一處隱蔽的房頂上張望蚀浆。 院中可真熱鬧,春花似錦搜吧、人聲如沸市俊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽摆昧。三九已至,卻和暖如春蜒程,著一層夾襖步出監(jiān)牢的瞬間绅你,已是汗流浹背伺帘。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留勇吊,地道東北人曼追。 一個(gè)月前我還...
    沈念sama閱讀 48,409評(píng)論 3 373
  • 正文 我出身青樓窍仰,卻偏偏與公主長(zhǎng)得像汉规,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子驹吮,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容