Flutter MVP 快速开发
2019-12-11 本文已影响0人
xiaoZhaoZhao
Flutter MVP 快速开发
练习做的项目,来做一些学习笔记,具体框架的使用方法就不做记录,网上很多,这里只做 MVP 框架搭建,框架借鉴了其他大佬写的一些代码,然后按照自己又做了些调整
基础配置
先来看下依赖的库都有哪些,也不一定全部都用到了 ,看自己项目需要吧
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
# 路由
fluro: ^1.5.1
# 简单数据存储
shared_preferences: ^0.5.3+4
# 依赖注入
get_it: ^3.0.1
# 吐司
fluttertoast: ^3.1.3
# 尺寸适配
flutter_screenutil: ^0.6.0
# 二维码条形码扫描
flutter_qr_reader: ^1.0.3
# 权限申请
permission_handler: ^3.0.0
# 网络请求
dio: ^2.0.1
# 手机网络状态监听
connectivity: ^0.4.3+7
# 消息总线
event_bus: ^1.1.0
# 刷新 加载
pull_to_refresh: ^1.5.4
# Rx
rxdart: ^0.21.0
在入口函数 main() 中进行部分框架的初始化:
void main() {
Router router = Router();
Routes.configureRoutes(router);
Application.router = router;
Application.setupLocator();
runApp(MyApp());
}
- 1、2、3 行是用来配置项目中路由的(fluro 框架)
- Application.setupLocator() 初始化 get_it 依赖注入框架,相当于 Android 中 dagger2
- 其中在 Application 中还做了屏幕适配配置
看下 Application 中的代码吧:
class Application {
static Router router;
static GlobalKey<NavigatorState> globalKey = GlobalKey();
static SharedPreferences sp;
static double screenWidth;
static double screenHeight;
static double statusBarHeight;
static GetIt getIt = GetIt.instance;
static initSp() async {
sp = await SharedPreferences.getInstance();
}
static initScreenUtil(BuildContext context){
ScreenUtil.instance = ScreenUtil(width: 750, height: 1334) ..init(context);
final size = MediaQuery.of(context).size;
Application.screenWidth = size.width;
Application.screenHeight = size.height;
Application.statusBarHeight = MediaQuery.of(context).padding.top;
}
static setupLocator(){
getIt.registerSingleton(NavigateService());
getIt.registerSingleton(DioRequest());
getIt.registerSingleton(ApiService());
getIt.registerSingleton(SettingPresenter());
getIt.registerSingleton(LoginPresenter());
getIt.registerSingleton(CollectionPresenter());
getIt.registerSingleton(BillPresenter());
}
}
看一下 main.dart 中的全部代码,基础框架初始化完成直接打开登录页面
void main() {
Router router = Router();
Routes.configureRoutes(router);
Application.router = router;
Application.setupLocator();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
navigatorKey: Application.getIt<NavigateService>().globalKey,
theme: ThemeData(///项目主题设置
brightness: Brightness.light,
primaryColor: Color(0xFF00A0E9),
splashColor: Colors.transparent,
backgroundColor: Color(0xFFF2F2F2),
),
home: LoginPage(),
onGenerateRoute: Application.router.generator,///路由生成
debugShowCheckedModeBanner: true,
);
}
}
开始搭建 MVP
base_contract.dart
abstract class IBaseView {
}
abstract class IBasePresenter<T extends IBaseView> {
/// 绑定 view
void attachView(T view);
/// 分离 view
void detachView();
}
abstract class IBaseModel {
}
base_view.dart(继承 base_contract.dart 中的 IBaseView)
import 'base_contract.dart';
class BaseView extends IBaseView {
void showLoading({String msg}) {
}
void closeLoading() {
}
void renderPage(Object object) {
}
void reload() {
}
void showError({String errorMsg}) {
}
void showDisConnect() {
}
}
接下来创建三个基础抽象类
base_page.dart(实现 base_contract.dart 中的 IBaseView)
abstract class BasePageState<T extends BasePresenter>
extends State<StatefulWidget> with AutomaticKeepAliveClientMixin implements BaseView {
T presenter = Application.getIt.get<T>();
bool _isPrepared = false;
@mustCallSuper///注意看这个注解,贼鸡儿重要,必须在子类中调用父类的这个方法,不然子类的 ///presenter 中会报 view 空指针
@override
Widget build(BuildContext context) {
_attachView();
if (!_isPrepared) {
Timer.run(() => preparePage());
_isPrepared = true;
}
return null;
}
@mustCallSuper
@override
void dispose() {
super.dispose();
_detachView();
}
/// 初始化一次 =》 用于 presenter 请求网络数据后调用 showDialog 拿不到合适的 context 报错
void preparePage();
@override
void reload() {}
@override
void renderPage(Object o) {}
@override
void showDisConnect() {}
@override
void showError({String errorMsg}) {
// TODO: implement showError
if (errorMsg != null) {
ToastUtil.showToast(errorMsg);
}
}
@override
void showLoading({String msg}) {
// TODO: implement showLoad
/// 把 dialog 的 show 从 普通页面里分离
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return LoadingDialog(
text: msg ?? '加载中',
);
});
}
@override
void closeLoading() {
// TODO: implement closeLoading
/// 必须和 showLoading 方法配对使用 ,避免 pop 当前页面
Navigator.pop(context);
}
void _attachView(){
if(null != presenter){
presenter.attachView(this);
}
}
void _detachView(){
if(null != presenter){
presenter.detachView();
}
}
@override
// TODO: implement wantKeepAlive
bool get wantKeepAlive => true;
}
base_presenter.dart (实现 base_contract.dart 中的 IBasePresenter)
import 'base_contract.dart';
abstract class BasePresenter<T extends IBaseView> implements IBasePresenter<T> {
T view;
@override
void attachView(T view) {
this.view = view;
}
@override
void detachView() {
if (null != view) {
this.view = null;
}
}
}
base_model.dart(实现 base_contract.dart 中的 IBaseModel)
class BaseModel implements IBaseModel {
ApiService apiService = Application.getIt.get<ApiService>();
}
应用(登录)
login_contract.dart
import 'package:flutter_mvp/base/base_contract.dart';
import 'package:rxdart/rxdart.dart';
abstract class ILoginView extends IBaseView {
/// 跳转到首页
void navigateHome();
/// 显示加载提示
void showLoading({String msg});
/// 关闭加载提示
void closeLoading();
/// 获取用户输入账号
String getAccount();
/// 获取用户输入密码
String getPassword();
}
abstract class ILoginPresenter extends IBasePresenter<ILoginView> {
/// 登录操作
void login();
}
abstract class ILoginModel extends IBaseModel{
/// 登录请求
Observable login(String account, String password);
}
login_page.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mvp/app/application.dart';
import 'package:flutter_mvp/base/base_page.dart';
import 'package:flutter_mvp/route/navigator_util.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'login_contract.dart';
import 'login_presenter.dart';
class LoginPage extends StatefulWidget {
@override
LoginPageState createState() => LoginPageState();
}
class LoginPageState extends BasePageState<LoginPresenter> implements ILoginView{
_navigateHomePage(BuildContext context) {
NavigatorUtil.goHomePage(context, replace: true);
}
TextEditingController _accountController = TextEditingController();
TextEditingController _passwordController = TextEditingController();
Color _btnColor = Color(0xff00A0E9);
bool _isObscureText = true;
void _setStatus(int status) {
setState(() {
switch (status) {
case 0:
_login();
break;
case 1:
_btnColor = Color(0xff0083BF);
break;
case 2:
_btnColor = Color(0xff00A0E9);
break;
}
});
}
void _closeOrOpenEye(){
setState(() {
_isObscureText = !_isObscureText;
});
}
@override
void initState() {
_accountController.text = '00113582010041';
_passwordController.text = '123456';
super.initState();
}
@override
Widget build(BuildContext context) {
Application.initScreenUtil(context);
super.build(context);
_accountController.addListener((){});
_passwordController.addListener((){});
return Scaffold(
body: Container(
color: Colors.white,
child: ListView(
children: <Widget>[
/// 手机号
Container(
margin: EdgeInsets.only(
left: ScreenUtil().setWidth(50),
right: ScreenUtil().setWidth(50),
top: ScreenUtil().setHeight(580)),
child: TextField(
controller: _accountController,
decoration: InputDecoration(
hintText: '请输入手机号',
),
style: TextStyle(fontSize: ScreenUtil().setSp(30)),
),
),
/// 密码
Container(
margin: EdgeInsets.only(
left: ScreenUtil().setWidth(50),
right: ScreenUtil().setWidth(50),
top: ScreenUtil().setHeight(50)),
child: Stack(
children: <Widget>[
TextField(
obscureText: _isObscureText,
textInputAction: TextInputAction.done,
controller: _passwordController,
decoration: InputDecoration(
hintText: '输入密码',
),
style: TextStyle(fontSize: ScreenUtil().setSp(30)),
),
/// 小眼睛
Positioned(
child: Center(
child: GestureDetector(
child: Image.asset('assets/images/${_isObscureText ? 'icon_eye_close' : 'icon_eye_open'}.png'),
onTap: _closeOrOpenEye,
),
),
right: 0,
top: ScreenUtil().setHeight(25),
)
],
),
),
/// 忘记密码
Row(),
/// 登录
GestureDetector(
onTapDown: (_) => _setStatus(1),
onTapUp: (_) => _setStatus(2),
onTap: () => _setStatus(0),
child: Container(
height: ScreenUtil().setHeight(80),
margin: EdgeInsets.only(
left: ScreenUtil().setWidth(75),
right: ScreenUtil().setWidth(75),
top: ScreenUtil().setHeight(227)),
child: Center(
child: Text(
'登录',
style: TextStyle(
color: Colors.white,
fontSize: ScreenUtil().setSp(34),
),
),
),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(
Radius.circular(
ScreenUtil().setWidth(10),
),
),
color: _btnColor,
),
),
),
],
),
),
);
}
void _login() async{
presenter.login();
}
@override
void preparePage() {
// TODO: implement preparePage
}
@override
String getAccount() {
// TODO: implement getAccount
return _accountController.text;
}
@override
String getPassword() {
// TODO: implement getPassword
return _passwordController.text;
}
@override
void navigateHome() {
// TODO: implement navigateHome
NavigatorUtil.goHomePage(context,replace: true);
}
}
login_presenter.dart
import 'package:flutter_mvp/base/base_presenter.dart';
import 'package:flutter_mvp/exception/exception.dart';
import 'package:flutter_mvp/pojo/response/login_rsp.dart';
import 'package:flutter_mvp/utils/sputil.dart';
import 'login_contract.dart';
import 'login_model.dart';
class LoginPresenter extends BasePresenter<ILoginView>
implements ILoginPresenter {
ILoginModel _loginModel;
LoginPresenter() {
_loginModel = LoginModel();
}
@override
void login() {
// TODO: implement login
view.showLoading(msg: '登录中...');
_loginModel.login(view.getAccount(), view.getPassword()).listen((dataMap) {
view.closeLoading();
print('登录响应数据:${dataMap.toString()}');
LoginRsp loginRsp = LoginRsp.fromJson(dataMap);
SpUtil.setToken(loginRsp.access_token);
}, onError: (error) {
view.closeLoading();
if (error is CommonException) {
print('登录错误: ${error.errorMsg}');
} else {
print('登录错误: ${error.toString()}');
}
}, onDone: () {
//实际开发中只有登录成功才跳转
view.navigateHome();
});
}
}
login_model.dart
import 'package:flutter_mvp/base/base_model.dart';
import 'package:rxdart/src/observables/observable.dart';
import 'login_contract.dart';
class LoginModel extends BaseModel implements ILoginModel {
@override
Observable login(String account, String password) {
// TODO: implement login
return apiService.login(account, password);
}
}
其他配置
- 图片适配:项目根目录下创建 assets/images/2.0x & assets/images/3.0x 路径,将不同倍图以同一命名分别放入 /images/、/images/2.0x/、/images/3.0x/ 路径下,然后在 pubspec.yaml 中 flutter: assets: 添加 - assets/images/
- Android 启动页:放置启动页图片到Android 路径 res 中相对应的 drawable 文件夹下,在 res/drawable/launch_background.xml 中,bitmap 标签的 src 修改为启动页图片名