写给读者
本人是一名安卓开发工程师(兼职IOS),最近学习了一下Flutter,整理了一套MVP开发框架。该框架我是基于Android的mvp思想编写的,还没在正式项目上使用,供大家参考。
项目介绍
包含mvp基础类,网络层封装(RxDart+Dio),数据解析等相关类的封装。
项目结构
common
- application.dart 项目全局属性
- base 项目基类
- constants 常量
- network 网络封装库
- route 路由管理
- utils 工具类
- widget 常用控件封装
contract
页面的view层接口和presenter层接口定义
main.dart
项目启动类
代码
application.dart 项目全局属性
class Application {
//路由管理
static Router router;
//依赖注入,全局单例
static GetIt getIt = GetIt.instance;
//全局用户信息
static UserEntity userEntity;
static SharedPreferences sp;
static initSp() async {
sp = await SharedPreferences.getInstance();
}
/// 是否登录
static bool isLogin() {
return userEntity != null;
}
static initScreenUtil(BuildContext context){
// ScreenUtil.init(context, width: 720, height: 1280);
// print('设置像素密度:${ScreenUtil.pixelRatio}');
// print('设置的高度:${ScreenUtil.screenHeight}');
// print('设置的宽度:${ScreenUtil.screenWidth}');
}
/// 依赖注入,全局单例对象
static setup(){
getIt.registerSingleton(new DioRequest());
getIt.registerSingleton(new UserModel());
}
}
【Model层相关类】
base_entity.dart Bean基类
class BaseEntity {
}
base_model.dart 发送网络请求(待完善)
class BaseModel {
//接口请求操作类-全局单例
DioRequest dioRequest = GetIt.instance();
//请求列表接口,每页数量
int pageCount = 10;
//参数
Map params = new Map();
void sendRequest(Stream stream, RxObserver observer) {
//发送请求
observer.requestHttp(stream);
}
}
dio_request.dart 封装的网络请求,结合了RxDart+Dio
class DioRequest {
/// http request methods
static const String GET = 'GET';
static const String POST = 'POST';
static const String PUT = 'PUT';
static const String PATCH = 'PATCH';
static const String DELETE = 'DELETE';
static DioFactory _dioFactory = DioFactory.instance;
Future request(String action, String url, {Map params, bool isJson}) async {
var formData;
if (!isJson) {
formData = params != null ? FormData.fromMap(params) : null;
} else {
formData = params;
}
// 获取本地token,添加请求头
if(Application.userEntity != null){
UserEntity user = Application.userEntity;
/// 动态添加headers
Map headers = new Map();
//token
headers['authorization'] = '${user.userId}_${user.token}';
//时间戳
headers['timestamp'] = Util.currentTimeMillis().toString();
//版本号
headers['version'] = '2.0';
_dioFactory.dio.options.headers.addAll(headers);
} else {
_dioFactory.dio.options.headers.remove('authorization');
}
Response response;
try {
switch (action) {
case GET:
response =
await _dioFactory.dio.get(url, queryParameters: params);
break;
case POST:
response = await _dioFactory.dio.post(url, data: formData);
break;
}
} on DioError catch (error) {
print('请求出错:' + error.toString());
// 请求错误处理
Response errorResponse;
if (error.response != null) {
errorResponse = error.response;
} else {
errorResponse = new Response(statusCode: ResultCode.NO_NETWORK);
}
// 请求超时
if (error.type == DioErrorType.CONNECT_TIMEOUT) {
errorResponse.statusCode = ResultCode.CONNECT_TIMEOUT;
}
// 一般服务器错误
else if (error.type == DioErrorType.RECEIVE_TIMEOUT) {
errorResponse.statusCode = ResultCode.RECEIVE_TIMEOUT;
}
throw CommonException(errorMsg: "网络异常");
}
return response.data;
}
Stream _get(String url, {Map params}) =>
Stream.fromFuture(request(GET, url, params: params)).asBroadcastStream();
Stream _post(String url, Map params, bool isJson) {
return Stream.fromFuture(request(POST, url, params: params, isJson: isJson)).asBroadcastStream();
}
Stream handlerGet(String requestUrl, {Map params}) {
return _get(requestUrl);
}
Stream handlerFormPost(String requestUrl, {Map params}) {
return _post(requestUrl, params, false);
}
Stream handlerJsonPost(String requestUrl, {Map params}) {
return _post(requestUrl, params, true);
}
}
rx_observer.dart 订阅网络请求,处理请求开始与结束的统一逻辑
typedef void OnSuccess(dynamic data);
typedef void OnFailure(String error);
class RxObserver {
//页面引用对象(显示进度框、提示语)
BaseView view;
//提示语
String msg;
//是否显示进度框
bool isShowDialog;
//成功回调
OnSuccess onSuccess;
//失败回调
OnFailure onFailure;
RxObserver(this.view, {this.onSuccess, this.onFailure, this.isShowDialog = true});
///http网络请求 [request] 接收 Stream 类型
void requestHttp(Stream request) {
request.doOnListen(() {
//开始网络请求,根据提示语显示弹框
if (view != null && isShowDialog) {
view.showLoading(msg: msg);
}
}).listen((data) {
//请求成功,返回结果,关闭弹框
if (view != null && isShowDialog) {
view.closeLoading();
}
//请求成功 获取数据data, data是返回结果json
BaseResponse response = BaseResponse.fromJson(data);
if (response.resultCode != 100) {
handlerError(response.resultMsg);
} else {
onSuccess(response.getData());
}
}, onError: (error) {
if (view != null && isShowDialog) {
view.closeLoading();
}
//请求失败
DioError e = error;
handlerError(e.message);
}, onDone: () {
//执行结束
});
// subject.cancel();
}
void handlerError(String error) {
if (view != null && isShowDialog) {
view.showError(errorMsg: error);
}
//请求失败
onFailure(error);
}
}
base_response.dart 接口返回结果基类
class BaseResponse {
dynamic data;
bool success;
int resultCode;
String resultMsg;
BaseResponse({this.data, this.success, this.resultCode, this.resultMsg});
BaseResponse.fromJson(Map json) {
data = json['data'];
success = json['success'];
resultCode = json['resultCode'];
resultMsg = json['resultMsg'];
}
/// 获取results对象
T getData() {
return EntityFactory.generateOBJ(data); //使用EntityFactory解析对象
}
/// 获取results数组
List getList() {
var newList = new List();
if (data != null) {
data.forEach((v) { //拼装List
newList.add(EntityFactory.generateOBJ(v));//使用EntityFactory解析对象
});
}
return newList;
}
Map toJson() {
final Map data = new Map();
data['data'] = this.data;
data['success'] = this.success;
data['resultCode'] = this.resultCode;
data['resultMsg'] = this.resultMsg;
return data;
}
}
【View层相关】
base_state.dart 页面基类(等同android/ios的BaseActivity/BaseViewController)
/// state基类
abstract class BaseState
extends State with AutomaticKeepAliveClientMixin implements BaseView {
P mPresenter;
//是否初始化
bool _isPrepared = false;
//请求dialog
LoadingDialog dialog;
@override
void initState() {
mPresenter = createPresenter();
_attachView();
super.initState();
}
///构建页面
@override
Widget build(BuildContext context) {
super.build(context);
// ScreenUtil.init(context, width: 750, height: 1334);
if (!_isPrepared) {
Timer.run(() => preparePage());
_isPrepared = true;
}
return Scaffold(
appBar: buildAppBar(),
body: buildPageLayout(),
);
}
Widget buildAppBar();
Widget buildPageLayout();
@mustCallSuper
@override
void dispose() {
super.dispose();
_detachView();
}
P createPresenter();
/// 初始化一次 =》 用于 presenter 请求网络数据后调用 showDialog 拿不到合适的 context 报错
void preparePage();
@override
void reload() {}
@override
void renderPage(Object o) {}
@override
void showDisConnect() {}
@override
void showError({String errorMsg}) {
if (errorMsg != null) {
ToastUtil.showToast(errorMsg);
}
}
@override
void showLoading({String msg}) {
/// 把 dialog 的 show 从 普通页面里分离
if (dialog == null) {
dialog = LoadingDialog(
text: msg ?? '加载中',
);
}
showDialog(
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return dialog;
});
}
@override
void closeLoading() {
/// 必须和 showLoading 方法配对使用 ,避免 pop 当前页面
if (dialog != null) {
Navigator.pop(context);
dialog = null;
}
}
void _attachView(){
if(null != mPresenter){
mPresenter.attachView(this);
}
}
void _detachView(){
if(null != mPresenter){
mPresenter.detachView();
}
}
//不会被销毁,占内存中
@override
bool get wantKeepAlive => true;
}
base_view.dart
abstract class BaseView {
void showLoading({String msg});
void closeLoading();
void renderPage(Object object);
void reload();
void showError({String errorMsg});
void showDisConnect();
}
【presenter相关】
base_presenter.dart
class BasePresenter {
V view;
/// 绑定视图,页面创建时调用
void attachView(V view) {
this.view = view;
}
/// 解绑视图,页面销毁时调用
void detachView() {
if (null != view) {
this.view = null;
}
}
}
以上是项目结构及相关重要的几个类介绍,源码在文章最后,下面看下实际使用:
main.dart 项目启动类
void main() {
//初始化路由管理
Router router = new Router();
Routes.configureRoutes(router);
Application.router = router;
//注册
Application.setup();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
brightness: Brightness.light,
primaryColor: Color(0xFF00A0E9),
splashColor: Colors.transparent,
backgroundColor: Color(0xFFFFFFFF),
),
home: SplashPage(),
onGenerateRoute: Application.router.generator,
// routes: {
// "/test" : (context) => new TestPage(),
// },
);
}
}
以登录页为例
【model层】
user_entity.dart 登录信息bean
class UserEntity {
var appId;
var redisKey;
var userId;
var token;
UserEntity({this.appId, this.redisKey, this.userId, this.token});
UserEntity.fromJson(Map json) {
appId = json['appId'];
redisKey = json['redisKey'];
userId = json['userId'];
token = json['token'];
}
Map toJson() {
final Map data = new Map();
data['appId'] = this.appId;
data['redisKey'] = this.redisKey;
data['userId'] = this.userId;
data['token'] = this.token;
return data;
}
}
user_model.dart 用户model操作类
class UserModel extends BaseModel {
/// 登录
void login(String account, String password, RxObserver observer){
password = Util.generateMd5(password);
params.clear();
params['username'] = account;
params['password'] = password;
Stream stream = dioRequest.handlerFormPost(Url.login, params: params);
sendRequest(stream, observer);
}
}
【view层】
page_login.dart 登录页面
class LoginPage extends StatefulWidget {
@override
LoginPageState createState() => LoginPageState();
}
class LoginPageState extends BaseState implements ILoginView{
bool isPasswordLogin = false; //是否密码登录,默认验证码登录
String _phoneNumber;
String _password;
@override
void initState() {
super.initState();
}
@override
Widget buildAppBar() {
return AppBar(
backgroundColor: Colors.white,
iconTheme: IconThemeData(color: Colors.black),
elevation: 0,
);
}
@override
Widget buildPageLayout() {
return Container(
color: Colors.white,
height: double.infinity,
padding: EdgeInsets.only(left: 20, right: 20),
child: Stack(
alignment: AlignmentDirectional.center,
children: [
Positioned(
top: 0,
left: 0,
bottom: 0,
right: 0,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start, //左侧对齐
children: [
//登录图标
Row(
children: [
Image.asset("images/login_logo.png", width: UIUtil.getWidth(61), height: UIUtil.getHeight(61),),
UIUtil.createText("登录", 24, AppColor.black4, isBold: true, margin: EdgeInsets.only(left: 7)),
InkWell(
child: UIUtil.createText(isPasswordLogin ? "验证码登录" : "密码登录",
12, AppColor.grayText, isBold: true, margin: EdgeInsets.only(left: 10)),
onTap: (){
setState(() {
isPasswordLogin = !isPasswordLogin;
});
},
)
],
),
//提示
UIUtil.createText("未注册手机号验证后即完成注册", 12, AppColor.grayText, margin: EdgeInsets.only(top: 56, bottom: 10)),
//手机号
ITextField(
keyboardType: ITextInputType.phone,
hintText: '请输入手机号',
hintStyle: TextStyle(color: AppColor.grayText),
textStyle: TextStyle(color: AppColor.black4),
fieldCallBack: (content) {
_phoneNumber = content;
print(_phoneNumber);
},
),
//密码
Container(
child: Stack(
children: [
ITextField(
margin: EdgeInsets.only(top: 20),
keyboardType: ITextInputType.password,
hintText: isPasswordLogin ? '请输入密码' : '请输入验证码',
hintStyle: TextStyle(color: AppColor.grayText),
textStyle: TextStyle(color: AppColor.black4),
fieldCallBack: (content) {
_password = content;
print(_password);
},
),
Positioned(
bottom: 0,
right: 10,
child: CustomCountdown((){
print("请求验证码");
}),
)
],
),
),
//登录
Container(
margin: EdgeInsets.only(top: 120),
width: double.infinity,
height: UIUtil.getHeight(50),
child: RaisedButton(
child: Text("登录", style: TextStyle(fontSize: UIUtil.getSp(16)),),
textColor: AppColor.black4,
color: AppColor.theme,
shape:RoundedRectangleBorder(borderRadius: BorderRadius.circular(25.0)),
onPressed: _login,
),
),
//注册视图
isPasswordLogin ?
Container(
margin: EdgeInsets.only(top: 10, left: 10, bottom: UIUtil.getHeight(82)),
child: Row(
children: [
UIUtil.createText("没有账号,", 12, AppColor.black6, isBold: true),
InkWell(
child: UIUtil.createText("去注册", 12, AppColor.theme, isBold: true),
onTap: (){
print("去注册");
},
),
Spacer(
flex: 1,
),
InkWell(
child: UIUtil.createText("忘记密码?", 12, AppColor.grayText, isBold: true, textAlign: TextAlign.end),
onTap: (){
print("忘记密码");
},
),
],
),
) : Text(""),
],
),
),
),
//底部按钮
Positioned(
// width: double.infinity,
bottom: 30,
child: Row(
children: [
Text.rich(TextSpan(
children: [
TextSpan(
text: "《服务协议》",
style: TextStyle(
color: AppColor.theme
),
// recognizer: _tapRecognizer
),
TextSpan(
text: "和",
style: TextStyle(
color: AppColor.grayText
),
),
TextSpan(
text: "《隐私政策》",
style: TextStyle(
color: AppColor.theme
),
// recognizer: _tapRecognizer
),
]
)),
],
),
)
],
),
);
}
void _login() {
mPresenter.login();
}
@override
void preparePage() {
// TODO: implement preparePage
}
@override
String getAccount() {
return _phoneNumber;
}
@override
String getPassword() {
return _password;
}
@override
LoginPresenter createPresenter() {
return LoginPresenter();
}
@override
void loginSuccess(UserEntity user) {
Application.userEntity = user;
//存储用户信息
String userJson = Util.json2String(user.toJson());
SpUtil.setString(SPKey.USER, userJson);
RouteUtil.goMainPage(context, replace: true);
}
}
login_page_contract.dart 登录页面的view接口和presenter接口定义
abstract class ILoginView extends BaseView {
/// 获取用户输入账号
String getAccount();
/// 获取用户输入密码
String getPassword();
/// 登录成功
void loginSuccess(UserEntity entity);
}
abstract class ILoginPresenter extends BasePresenter {
/// 登录
void login();
/// 获取验证码
void getPhoneCode();
}
【presenter层】
login_presenter.dart
class LoginPresenter extends ILoginPresenter {
@override
void login() {
// TODO: implement login
String account = view.getAccount();
String password = view.getPassword();
if (account == null || account.length == 0) {
view.showError(errorMsg: '请输入手机号');
return;
}
if (password == null || password.length == 0) {
view.showError(errorMsg: '请输入密码');
return;
}
Application.getIt.get().login(account, password, new RxObserver(view,
onSuccess: (data) {
print(data);
view.loginSuccess(data);
},
onFailure: (error) {
}
));
}
@override
void getPhoneCode() {
// TODO: implement getPhoneCode
}
}
补充介绍 base_list_page.dart 列表基类
开发中,经常有列表页面,所有会存在一些公共逻辑,比如下拉刷新,加载更多,空视图、失败视图等。base_list_page可以省去一些公共代码,减少开发工作量。
/// 列表页面的state基类
abstract class BaseListPageState extends BaseState
implements BaseListView {
List list = [];
RefreshController _refreshController = RefreshController(initialRefresh: false);
int page = 1;
int total;
bool isFailure = false;
void _onRefresh() async {
page = 1;
requestListData();
}
void _onLoading() async {
requestListData();
}
@override
int getPage() {
return page;
}
@override
void listFailure() {
if (page == 1) {
isFailure = true;
_refreshController.refreshCompleted();
} else {
_refreshController.loadFailed();
}
setState(() {
});
}
@override
void listSuccess(ListEntity result) {
total = result.count;
if (page == 1) {
isFailure = false;
setEnableLoadMore(true);
list.clear();
list.addAll(result.getList());
_refreshController.refreshCompleted();
} else {
list.addAll(result.getList());
_refreshController.loadComplete();
}
page++;
if (list == null || list.length == 0) { //列表为空
return;
}
if (list.length >= total) {//没有更多数据了
_refreshController.loadNoData();
}
setState(() {
});
}
//是否开启下拉刷新
bool enableRefresh = true;
//是否开启上拉加载
bool enableLoadMore = true;
/// 构建一个带刷新的列表视图
Widget buildListView() {
return SmartRefresher (
enablePullDown: enableRefresh,
enablePullUp: enableLoadMore,
// WaterDropHeader、ClassicHeader、CustomHeader、LinkHeader、MaterialClassicHeader、WaterDropMaterialHeader
header: ClassicHeader(
height: 45.0,
releaseText: '松开手刷新',
refreshingText: '刷新中',
completeText: '刷新完成',
failedText: '刷新失败',
idleText: '下拉刷新',
),
// ClassicFooter、CustomFooter、LinkFooter、LoadIndicator
footer: CustomFooter(
builder: (BuildContext context, LoadStatus mode) {
Widget body;
if (mode == LoadStatus.idle) {
body = Text("上拉加载更多");
}
else if (mode == LoadStatus.loading) {
body = CupertinoActivityIndicator();
}
else if (mode == LoadStatus.failed) {
body = Text("加载失败,点击重试");
}
else {
body = Text("已经到底了");
}
return Container(
height: 55.0,
child: Center(child: body),
);
},
),
controller: _refreshController,
onRefresh: _onRefresh,
onLoading: _onLoading,
child: contentView(),
);
}
///有返回列表视图。无数据,返回空视图
Widget contentView() {
if (page == 1 && isFailure) {
return HintWidget(HintWidget.ERROR, function: () {
/// 列表请求失败时,重新请求
requestListData();
});
}
if (list == null || list.length == 0) {
//第一次返回加载中,total有值时,list=0返回空视图
return HintWidget(total != null ? HintWidget.EMPTY : HintWidget.LOADING);
} else {
//返回列表视图
return ListView.builder(
itemBuilder: (BuildContext context, int index){
return GestureDetector(
onTap: () {
//item Click
onItemClick(context, index);
},
child: buildItem(context, index),
);
},
itemCount: list.length,
// itemExtent: 100.0,
// cacheExtent: 10,
);
}
}
//构建列表行布局
Widget buildItem(BuildContext context, int index);
//列表行点击事件
void onItemClick(BuildContext context, int index);
//请求列表数据
void requestListData();
//是否开启加载更多
void setEnableLoadMore(enable) {
enableLoadMore = enable;
}
//是否开启下拉刷新
void setEnableRefresh(enable) {
enableRefresh = enable;
}
// don't forget to dispose refreshController
@override
void dispose() {
_refreshController.dispose();
super.dispose();
}
}
base_list_view.dart
abstract class BaseListView extends BaseView {
//加载成功
void listSuccess(ListEntity result);
//加载失败
void listFailure();
//获取当前页数
int getPage();
}
使用示例
/// 车辆列表
class CarListPage extends StatefulWidget {
@override
_CarListPageState createState() => _CarListPageState();
}
/// 指定泛型 ,最后一个类型为列表对应的实体类
class _CarListPageState extends BaseListPageState implements ICarListView{
@override
Widget buildAppBar() {
// TODO: implement buildAppBar
return new AppBar(title: Text("列表页"),);
}
@override
Widget buildItem(BuildContext context, int index) {
// TODO: implement buildItem
return OnlineCarItem(list[index], (carEntity){
if (Application.isLogin()) {
//根据收藏状态,调用不同接口
if (Util.equals(carEntity.isFollow, "1")) {
mPresenter.cancelCollectionCar(carEntity);
} else {
mPresenter.collectionCar(carEntity);
}
} else {
RouteUtil.goLoginPage(context);
}
});
}
@override
Widget buildPageLayout() {
// TODO: implement buildPageLayout
return buildListView();
}
@override
CarListPresenter createPresenter() {
// TODO: implement createPresenter
return new CarListPresenter();
}
@override
void onItemClick(BuildContext context, int index) {
// TODO: implement onItemClick
CarEntity carEntity = list[index];
print("点击:$carEntity");
RouteUtil.goDetailPage(context, carEntity.id.toString());
}
@override
void preparePage() {
// TODO: implement preparePage
requestListData();
}
@override
void requestListData() {
// TODO: implement requestListData
mPresenter.getList();
}
@override
void collectionSuccess() {
// TODO: implement likeSuccess
// 操作成功,刷新列表
setState(() {
});
}
@override
void cancelCollectionSuccess() {
// TODO: implement cancelCollectionSuccess
// 操作成功,刷新列表
setState(() {
});
}
}
car_list_page_contract.dart
abstract class ICarListView extends BaseListView {
/// 收藏成功
void collectionSuccess();
/// 取消收藏成功
void cancelCollectionSuccess();
}
abstract class IListPresenter extends BasePresenter {
/// 获取列表
void getList();
/// 收藏车辆
void collectionCar(CarEntity carEntity);
/// 取消收藏车辆
void cancelCollectionCar(CarEntity carEntity);
}
car_list_presenter.dart
class CarListPresenter extends IListPresenter {
@override
void getList() {
Application.getIt.get().getCarList(view.getPage(), new RxObserver(view,
onSuccess: (data) {
view.listSuccess(data);
},
onFailure: (error) {
view.listFailure();
}
));
}
@override
void collectionCar(CarEntity carEntity) {
Application.getIt.get().collection(carEntity.id.toString(), new RxObserver(view,
onSuccess: (data) {
//车辆关注状态更改
carEntity.isFollow = carEntity.isFollow.toString().endsWith("1") ? "2" : "1";
view.collectionSuccess();
},
onFailure: (error) {
}
));
}
@override
void cancelCollectionCar(CarEntity carEntity) {
Application.getIt.get().cancelCollection(carEntity.id.toString(), new RxObserver(view,
onSuccess: (data) {
//车辆关注状态更改
carEntity.isFollow = carEntity.isFollow.toString().endsWith("1") ? "2" : "1";
view.cancelCollectionSuccess();
},
onFailure: (error) {
}
));
}
}
【用到的一些框架】
页面路由管理
https://github.com/theyakka/fluro
简单数据存储
https://pub.dev/packages/shared_preferences
网络请求
https://github.com/flutterchina/dio
Rx
https://pub.dev/packages/rxdart
依赖注入(单例)
https://pub.flutter-io.cn/packages/get_it
下拉刷新 加载
https://pub.dev/packages/pull_to_refresh
图片加载
https://pub.dev/packages/cached_network_image
【最后】
现阶段对Flutter的很多地方理解不够,能力有限,如有不合适需要改善的地方,可以多提宝贵意见!
【项目地址】
https://github.com/zhengqiyao93/Flutter_Mvp