参考文档:https://medium.com/@develodroid/flutter-iv-mvp-architecture-e4a979d9f47e
原GitHub地址:https://github.com/fabiomsr/Flutter-StepByStep/tree/master/step3
英文好的同学,可以直接看参考文档。这篇文章只是记录下我自己学习的笔记。
本篇文章,旨在利用MVP模型来搭建一个Flutter demo项目,其中的UI设计都挺简单的,采用一个ListView来显示联系人列表页面。其中的涉及到的知识有:
MVP模型包含三个重要的模块:Model层,View层,Presenter层。分为这三层主要的作用就是将业务逻辑代码与视图布局界面显示代码分离。
对应代码在lib/data文件夹中,新建一个contact_data.dart文件。里面主要做的工作是:
import 'dart:async';
/// 联系人的类基本信息
class Contact {
final String fullName;
final String email;
const Contact({this.fullName, this.email});
Contact.fromMap(Map map)
:
fullName="${map['name']['first']} ${map['name']['last']}",
email="${map['email']}";
}
/// 联系人的仓库,可以从网络上拉取信息,也可以从本地拉取,看实现的方式了
abstract class ContactRepository {
Future> fetch();
}
/// 信息拉取异常类
class FetchException implements Exception {
String _message;
FetchException(this._message);
@override
String toString() {
return "Exception:$_message";
}
}
其中import ‘dart:async’;是用来引入dart语言提供的异步框架,具体使用可以看官网教程dart异步框架。
本地测试的数据仓库。contact_data_mock.dart文件中。
import 'contact_data.dart';
const kContacts = [
Contact(fullName: 'Romain Hoogmoed', email: '[email protected]'),
Contact(fullName: 'Emilie Olsen', email: '[email protected]'),
Contact(fullName: 'Téo Lefevre', email: 'té[email protected]'),
Contact(fullName: 'Nicole Cruz', email: '[email protected]'),
Contact(fullName: 'Ramna Peixoto', email: '[email protected]'),
Contact(fullName: 'Jose Ortiz', email: '[email protected]'),
Contact(fullName: 'Alma Christensen', email: '[email protected]'),
Contact(fullName: 'Sergio Hill', email: '[email protected]'),
Contact(fullName: 'Malo Gonzalez', email: '[email protected]'),
Contact(fullName: 'Miguel Owens', email: '[email protected]'),
Contact(fullName: 'Lilou Dumont', email: '[email protected]'),
Contact(fullName: 'Ashley Stewart', email: '[email protected]'),
Contact(fullName: 'Roman Zhang', email: '[email protected]'),
Contact(fullName: 'Ryan Roberts', email: '[email protected]'),
Contact(fullName: 'Sadie Thomas', email: '[email protected]'),
Contact(fullName: 'Belen Serrano', email: '[email protected] ')
];
class ContactMockRepository implements ContactRepository {
@override
Future> fetch() {
// 直接返回一个Future
return Future.value(kContacts);
}
}
利用http框架,通过url从网络上拉取json数据,然后用JsonDecoder进行解析。contact_data_impl.dart文件中。
import 'dart:async';
import 'dart:convert';
// http请求框架
import 'package:http/http.dart' as http;
import 'contact_data.dart';
class RandomUserRepository implements ContactRepository {
// 请求地址
static const _kRandomUserUrl = 'http://api.randomuser.me/?results=15';
final _jsonDecoder = JsonDecoder();
@override
Future> fetch() async {
var response = await http.get(_kRandomUserUrl);
var jsonBody = response.body;
var statusCode = response.statusCode;
if (statusCode < 200 || statusCode >= 300 || jsonBody == null) {
throw new FetchException(
"Error while getting contacts. status:$statusCode, Error:${response.reasonPhrase}");
}
final container = _jsonDecoder.convert(jsonBody);
final List contactItems = container['results'];
return contactItems.map((contactItem)=>Contact.fromMap(contactItem)).toList();
}
}
关于http框架依赖引入问题,需要在项目配置文件pubspec.yaml中添加一行,配置http的版本号。
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.2
# http网络请求框架
http: ^0.12.0
主要就是在presenter进行数据处理之后,进行页面数据显示。为了更好的体现代码的逻辑,通常需要把View层和Presenter层中的逻辑抽离出接口。所以,就需要另外的Contract接口类。体现在contact_contract.dart文件中。
import '../../data/contact_data.dart';
/// ListView 的接口类
abstract class ContactListViewContract {
/// 在联系人信息加载完成之后
void onLoadContactComplete(List items);
/// 信息加载发生错误
void onLoadContactError();
}
abstract class ContactPresenterContract {
/// 加载联系人信息,一般是访问model层封装的数据请求方法
void loadContacts();
}
由于页面需要请求等待数据,然后再刷新列表显示的内容,所以需要继承StatefulWidget。具体代码在contact_view.dart中。
import 'package:flutter/material.dart';
import '../../data/contact_data.dart';
import 'contact_contract.dart';
import 'contact_presenter.dart';
class ContactPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Contacts"),
),
body: ContactListView(),
);
}
}
class ContactListView extends StatefulWidget {
@override
_ContactListViewState createState() => _ContactListViewState();
}
class _ContactListViewState extends State
implements ContactListViewContract {
ContactPresenterContract _presenter;
List _contacts;
/// 是否在查找联系人数据
bool _isSearching;
_ContactListViewState() {
_presenter = new ContactPresenter(this);
}
@override
void initState() {
super.initState();
_isSearching = true;
// 异步加载联系人数据
_presenter.loadContacts();
}
@override
Widget build(BuildContext context) {
Widget widget;
if (_isSearching) {
widget = Center(
child: Padding(
padding: EdgeInsets.all(10.0),
child: CircularProgressIndicator(),
),
);
} else {
widget = ListView.builder(
itemCount: _contacts.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: CircleAvatar(
child: Center(
child: Text(_contacts[index].fullName.substring(0, 1)),
),
),
title: Text(_contacts[index].fullName),
subtitle: Text(_contacts[index].email),
);
});
}
return widget;
}
@override
void onLoadContactComplete(List items) {
setState(() {
_contacts = items;
_isSearching = false;
});
}
@override
void onLoadContactError() {
//TODO 加载失败后进行的操作
}
}
在这一层,沟通model层和View层,具体的代码逻辑在contact_presenter.dart中。
import '../../data/contact_data.dart';
import '../../injection/injection.dart';
import 'contact_contract.dart';
class ContactPresenter implements ContactPresenterContract {
ContactListViewContract _view;
ContactRepository _repository;
ContactPresenter(this._view) : _repository = new Injector().contactRepository;
@override
void loadContacts() {
assert(_view != null);
_repository
.fetch()
.then((items) => _view.onLoadContactComplete(items))
.catchError((onError) {
print(onError);
_view.onLoadContactError();
});
}
}
根据环境(测试环境或者生产环境,当然该项目只是模拟一下),配置不同的参数,例如数据源等。所以,本项目提供了一个injector配置数据源。在injection/injection.dart中。
import '../data/contact_data.dart';
import '../data/contact_data_impl.dart';
import '../data/contact_data_mock.dart';
enum Flavor {
// 生产环境
PRO,
// 测试环境
MOCK,
}
/// 简单的依赖注入
class Injector {
static final Injector _injector = Injector._internal();
static Flavor _flavor;
static void configure(Flavor flavor) {
_flavor = flavor;
}
factory Injector() {
return _injector;
}
// 内部初始化
Injector._internal();
ContactRepository get contactRepository {
switch (_flavor) {
case Flavor.PRO:
return new RandomUserRepository();
case Flavor.MOCK:
return new ContactMockRepository();
default:
return null;
}
}
}
需要在main.dart中配置当前采用的环境。具体代码如下:main.dart
import 'package:flutter/material.dart';
import 'injection/injection.dart';
import 'modules/contract/contact_view.dart';
void main() => runApp(MyApp(Flavor.PRO));
@immutable
class MyApp extends StatelessWidget {
final Flavor _flavor;
MyApp(this._flavor) {
Injector.configure(_flavor);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "Flutter MVP Demo",
theme: ThemeData(
primarySwatch: Colors.lightBlue,
),
home: ContactPage(),
);
}
}
官网异步加载教程:https://dart.dev/tutorials/language/futures?source=post_page---------------------------
完整项目代码提供下载:https://github.com/shengleiRain/flutter_app/tree/master/mvp_demo