在 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/bbs 或 flutter-dev.com/bbs 與大家一起討論和學(xué)習(xí) 虎敦。