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

在 Android 應(yīng)用程序開發(fā)過程中,我們經(jīng)常會用到一些所謂的架構(gòu)方法,如:mvp,mvvm,clean等。之所以這些方法會被推崇是因為他們可以大大的解耦我們的代碼的功能模塊,讓我們的代碼在項目中后期更容易擴展和維護。

我個人比較推薦 mvp,主要是因為其相對比較簡單且易上手,這次我將給大家介紹如何在 Flutter 中使用 mvp 來組織項目的功能模塊。為了演示方便,我選擇了一個比較簡單的通訊錄列表來為大家做演示。

MVP

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

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


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

abstract class IPresenter{
  init();
}

對,這兩個類就是如此簡單。

數(shù)據(jù)源

首先我們不急著寫 UI 代碼,先保持 main.dart 文件不變。我們首先要定義一個 Contact 類,用于表示通訊錄中的每一項,接著還要定義一個數(shù)據(jù)倉庫接口 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 有兩個字段 fullName 和 email 。ContactRepository 有一個 fetch 方法,用于獲取通訊錄列表。

既然定義了 ContactRepository 接口,接下來編寫它的實現(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 的功能就是在前期提供測試的假數(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();
}

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

Presenter 的實現(xiàn)

接下來我們首先實現(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 對象。這樣一來 View 和 Presenter 兩者就綁定到了一起。接著在 init 方法中初始化了 _repository 對象。

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

View 的實現(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;
  }
}

這段代碼有些長,我們分段來看。

首先是類 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ù)后會調(diào)用 _ContactListState 的 onLoadContactsComplete 方法來更新 UI 。

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

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

在上面我們使用的是 MockContactRepository 提供的假數(shù)據(jù),接著我們定義一個 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 兩個類,

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 文件內(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"),
  ];

最后需要改動的地方是 ContactPresenter 類的 init 方法,

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

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

總結(jié)

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

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容