前言
当前案例 Flutter SDK版本:3.22.2
每当我们开始一个新项目,都会 引入常用库、封装工具类,配置环境等等,我参考了一些文档,将这些内容整合、简单修改、二次封装,得到了一个开箱即用的Flutter开发模版,即使看不懂封装的工具对象原理,也没关系,模版化的使用方式,小白也可以快速开发Flutter项目。
快速上手
用到的依赖库
dio: ^5.4.3+1 // 网络请求fluro: ^2.0.5 // 路由pull_to_refresh: ^2.0.0 // 下拉刷新 / 上拉加载更多
修改规则
默认使用的是Flutter团队制定的规则,但每个开发团队规则都不一样,违反规则的地方会出现黄色波浪下划线,比如我定义常量喜欢字母全部大写,这和默认规则不符;
修改 Flutter项目里的 analysis_options.yaml 文件,找到 rules,添加以下配置;
rules:use_key_in_widget_constructors: falseprefer_const_constructors: falsepackage_names: null
修改前
修改后
MVVM
- MVVM 设计模式,相信大家应该不陌生,我简单说一下每层主要负责做什么;
- Model: 数据相关操作;
- View:UI相关操作;
- ViewModel:业务逻辑相关操作。
持有关系:
View持有 ViewModel;
Model持有ViewModel;
ViewModel持有View;
ViewModel持有Model;
注意:这种持有关系,有很高的内存泄漏风险,所以我在基类的 dispose() 中进行了销毁,子类重写一定要调用 super.dispose();
/// BaseStatefulPageState的子类,重写 dispose()
/// 一定要执行父类 dispose(),防止内存泄漏
@override
void dispose() {/// 销毁顺序/// 1、Model 销毁其持有的 ViewModelif(viewModel?.pageDataModel?.data is BaseModel?) {BaseModel? baseModel = viewModel?.pageDataModel?.data as BaseModel?;baseModel?.onDispose();}if(viewModel?.pageDataModel?.data is BasePagingModel?) {BasePagingModel? basePagingModel = viewModel?.pageDataModel?.data as BasePagingModel?;basePagingModel?.onDispose();}/// 2、ViewModel 销毁其持有的 View/// 3、ViewModel 销毁其持有的 ModelviewModel?.onDispose();/// 4、View 销毁其持有的 ViewModelviewModel = null;/// 5、销毁监听App生命周期方法lifecycleListener?.dispose();super.dispose();
}
基类放在文章最后说,这里先忽略;
Model
class HomeListModel extends BaseModel {... ... ValueNotifier<int> tapNum = ValueNotifier<int>(0); // 点击次数@overridevoid onDispose() {tapNum.dispose();super.onDispose();}... ...}... ...
View
class HomeView extends BaseStatefulPage<HomeViewModel> {HomeView({super.key});@overrideHomeViewState createState() => HomeViewState();
}class HomeViewState extends BaseStatefulPageState<HomeView, HomeViewModel> {@overrideHomeViewModel viewBindingViewModel() {/// ViewModel 和 View 相互持有return HomeViewModel()..viewState = this;}/// 初始化 页面 属性@overridevoid initAttribute() {... ...}/// 初始化 页面 相关对象绑定@overridevoid initObserver() {... ...}@overridevoid dispose() {... ... /// BaseStatefulPageState的子类,重写 dispose()/// 一定要执行父类 dispose(),防止内存泄漏super.dispose();}ValueNotifier<int> tapNum = ValueNotifier<int>(0);@overrideWidget appBuild(BuildContext context) {... ...}/// 是否保存页面状态@overridebool get wantKeepAlive => true;}
ViewModel
class HomeViewModel extends PageViewModel {HomeViewState? state;@overrideonCreate() {/// 转化成 对应View 状态类型state = viewState as HomeViewState;... ... /// 初始化 网络请求requestData();}@overrideonDispose() {... .../// 别忘了执行父类的 onDisposesuper.onDispose();}/// 请求数据@overrideFuture<PageViewModel?> requestData({Map<String, dynamic>? params}) async {... ...}
}
网络请求
Get请求
class HomeRepository {/// 获取首页数据Future<PageViewModel> getHomeData({required PageViewModel pageViewModel,CancelToken? cancelToken,int curPage = 0,}) async {try {Response response = await DioClient().doGet('project/list/$curPage/json?cid=294', cancelToken: cancelToken);if(response.statusCode == REQUEST_SUCCESS) {/// 请求成功pageViewModel.pageDataModel?.type = NotifierResultType.success;/// ViewModel 和 Model 相互持有HomeListModel model = HomeListModel.fromJson(response.data);model.vm = pageViewModel;pageViewModel.pageDataModel?.data = model;} else {/// 请求成功,但业务不通过,比如没有权限pageViewModel.pageDataModel?.type = NotifierResultType.unauthorized;pageViewModel.pageDataModel?.errorMsg = response.statusMessage;}} on DioException catch (dioEx) {/// 请求异常pageViewModel.pageDataModel?.type = NotifierResultType.dioError;pageViewModel.pageDataModel?.errorMsg = dioErrorConversionText(dioEx);} catch (e) {/// 未知异常pageViewModel.pageDataModel?.type = NotifierResultType.fail;pageViewModel.pageDataModel?.errorMsg = (e as Map).toString();}return pageViewModel;}}
Post请求
class PersonalRepository {/// 注册Future<PageViewModel> registerUser({required PageViewModel pageViewModel,Map<String, dynamic>? params,CancelToken? cancelToken,}) async {try {Response response = await DioClient().doPost('user/register',params: params,cancelToken: cancelToken,);if(response.statusCode == REQUEST_SUCCESS) {/// 请求成功pageViewModel.pageDataModel?.type = NotifierResultType.success; // 请求成功/// ViewModel 和 Model 相互持有UserInfoModel model = UserInfoModel.fromJson(response.data)..isLogin = false;model.vm = pageViewModel;pageViewModel.pageDataModel?.data = model;} else {/// 请求成功,但业务不通过,比如没有权限pageViewModel.pageDataModel?.type = NotifierResultType.unauthorized;pageViewModel.pageDataModel?.errorMsg = response.statusMessage;}} on DioException catch (dioEx) {/// 请求异常pageViewModel.pageDataModel?.type = NotifierResultType.dioError;pageViewModel.pageDataModel?.errorMsg = dioErrorConversionText(dioEx);} catch (e) {/// 未知异常pageViewModel.pageDataModel?.type = NotifierResultType.fail;pageViewModel.pageDataModel?.errorMsg = (e as Map).toString();}return pageViewModel;}/// 登陆Future<PageViewModel> loginUser({required PageViewModel pageViewModel,Map<String, dynamic>? params,CancelToken? cancelToken,}) async {try {Response response = await DioClient().doPost('user/login',params: params,cancelToken: cancelToken,);if(response.statusCode == REQUEST_SUCCESS) {/// 请求成功pageViewModel.pageDataModel?.type = NotifierResultType.success;/// ViewModel 和 Model 相互持有UserInfoModel model = UserInfoModel.fromJson(response.data)..isLogin = true;model.vm = pageViewModel;pageViewModel.pageDataModel?.data = model;} else {/// 请求成功,但业务不通过,比如没有权限pageViewModel.pageDataModel?.type = NotifierResultType.unauthorized;pageViewModel.pageDataModel?.errorMsg = response.statusMessage;}} on DioException catch (dioEx) {/// 请求异常pageViewModel.pageDataModel?.type = NotifierResultType.dioError;pageViewModel.pageDataModel?.errorMsg = dioErrorConversionText(dioEx);} catch (e) {/// 未知异常pageViewModel.pageDataModel?.type = NotifierResultType.fail;pageViewModel.pageDataModel?.errorMsg = (e as Map).toString();}return pageViewModel;}}
分页数据请求
class MessageRepository {/// 分页列表Future<PageViewModel> getMessageData({required PageViewModel pageViewModel,CancelToken? cancelToken,int curPage = 0,}) async {try {Response response = await DioClient().doGet('article/list/$curPage/json', cancelToken: cancelToken);if(response.statusCode == REQUEST_SUCCESS) {/// 请求成功pageViewModel.pageDataModel?.type = NotifierResultType.success;/// 有分页pageViewModel.pageDataModel?.isPaging = true;/// 分页代码pageViewModel.pageDataModel?.correlationPaging(pageViewModel, MessageListModel.fromJson(response.data));} else {/// 请求成功,但业务不通过,比如没有权限pageViewModel.pageDataModel?.type = NotifierResultType.unauthorized;pageViewModel.pageDataModel?.errorMsg = response.statusMessage;}} on DioException catch (dioEx) {/// 请求异常pageViewModel.pageDataModel?.type = NotifierResultType.dioError;pageViewModel.pageDataModel?.errorMsg = dioErrorConversionText(dioEx);} catch (e) {/// 未知异常pageViewModel.pageDataModel?.type = NotifierResultType.fail;pageViewModel.pageDataModel?.errorMsg = (e as Map).toString();}return pageViewModel;}}
剩下的 ResultFul API 风格请求,我就不一一演示了,DioClient 里都封装好了,昭葫芦画瓢就好。
ResultFul API 风格
GET:从服务器获取一项或者多项数据
POST:在服务器新建一个资源
PUT:在服务器更新所有资源
PATCH:更新部分属性
DELETE:从服务器删除资源
刷新页面
NotifierPageWidget
这个组件是我封装的,和 ViewModel 里的 PageDataModel 绑定,当PageDataModel里的数据发生改变,就可以通知 NotifierPageWidget 刷新;
enum NotifierResultType {// 不检查notCheck,// 加载中loading,// 请求成功success,// 这种属于请求成功,但业务不通过,比如没有权限unauthorized,// 请求异常dioError,// 未知异常fail,
}typedef NotifierPageWidgetBuilder<T extends BaseChangeNotifier> = WidgetFunction(BuildContext context, PageDataModel model);/// 这个是配合 PageDataModel 类使用的
class NotifierPageWidget<T extends BaseChangeNotifier> extends StatefulWidget {NotifierPageWidget({super.key,required this.model,required this.builder,});/// 需要监听的数据观察类final PageDataModel? model;final NotifierPageWidgetBuilder builder;@override_NotifierPageWidgetState<T> createState() => _NotifierPageWidgetState<T>();
}class _NotifierPageWidgetState<T extends BaseChangeNotifier>extends State<NotifierPageWidget<T>> {PageDataModel? model;/// 刷新UIrefreshUI() => setState(() {model = widget.model;});/// 对数据进行绑定监听@overridevoid initState() {super.initState();model = widget.model;// 先清空一次已注册的Listener,防止重复触发model?.removeListener(refreshUI);// 添加监听model?.addListener(refreshUI);}@overridevoid didUpdateWidget(covariant NotifierPageWidget<T> oldWidget) {super.didUpdateWidget(oldWidget);if (oldWidget.model != widget.model) {// 先清空一次已注册的Listener,防止重复触发oldWidget.model?.removeListener(refreshUI);model = widget.model;// 添加监听model?.addListener(refreshUI);}}@overrideWidget build(BuildContext context) {if (model?.type == NotifierResultType.notCheck) {return widget.builder(context, model!);}if (model?.type == NotifierResultType.loading) {return Center(child: Text('加载中...'),);}if (model?.type == NotifierResultType.success) {if (model?.data == null) {return Center(child: Text('数据为空'),);}if(model?.isPaging ?? false) {var lists = model?.data?.datas as List<BasePagingItem>?;if(lists?.isEmpty ?? false){return Center(child: Text('列表数据为空'),);};}return widget.builder(context, model!);}if (model?.type == NotifierResultType.unauthorized) {return Center(child: Text('业务不通过:${model?.errorMsg}'),);}/// 异常抛出,会在终端会显示,可帮助开发阶段,快速定位异常所在,/// 但会阻断,后续代码执行,建议 非开发阶段 关闭if(EnvConfig.throwError) {throw Exception('${model?.errorMsg}');}if (model?.type == NotifierResultType.dioError) {return Center(child: Text('dioError异常:${model?.errorMsg}'),);}if (model?.type == NotifierResultType.fail) {return Center(child: Text('未知异常:${model?.errorMsg}'),);}return Center(child: Text('请联系客服:${model?.errorMsg}'),);}@overridevoid dispose() {widget.model?.removeListener(refreshUI);super.dispose();}
}
使用
class HomeView extends BaseStatefulPage<HomeViewModel> {HomeView({super.key});@overrideHomeViewState createState() => HomeViewState();
}class HomeViewState extends BaseStatefulPageState<HomeView, HomeViewModel> { @overrideWidget appBuild(BuildContext context) {return Scaffold(... ... body: NotifierPageWidget<PageDataModel>(model: viewModel?.pageDataModel,builder: (context, dataModel) {final data = dataModel.data as HomeListModel?;... ... return Stack(children: [ListView.builder(padding: EdgeInsets.zero,itemCount: data?.datas?.length ?? 0,itemBuilder: (context, index) {return Container(width: MediaQuery.of(context).size.width,height: 50,alignment: Alignment.center,child: Text('${data?.datas?[index].title}'),);}),... ...],);}),);}}
ValueListenableBuilder
这个就是Flutter自带的组件,配合ValueNotifier使用,我主要用它做局部刷新;
class HomeView extends BaseStatefulPage<HomeViewModel> {HomeView({super.key});@overrideHomeViewState createState() => HomeViewState();
}class HomeViewState extends BaseStatefulPageState<HomeView, HomeViewModel> {... ... ValueNotifier<int> tapNum = ValueNotifier<int>(0);@overrideWidget appBuild(BuildContext context) {return Scaffold(appBar: AppBar(backgroundColor: AppBarTheme.of(context).backgroundColor,/// 局部刷新title: ValueListenableBuilder<int>(valueListenable: tapNum,builder: (context, value, _) {return Text('Home:$value',style: TextStyle(fontSize: 20),);},),... ... ),);}}
演示效果
路由
配置
class Routers {static FluroRouter router = FluroRouter();// 配置路由static void configureRouters() {router.notFoundHandler = Handler(handlerFunc: (_, __) {// 找不到路由时,返回指定提示页面return Scaffold(body: const Center(child: Text('404'),),);});// 初始化路由_initRouter();}// 设置页面// 页面标识static String root = '/';// 页面Astatic String pageA = '/pageA';// 页面Bstatic String pageB = '/pageB';// 页面Cstatic String pageC = '/pageC';// 页面Dstatic String pageD = '/pageD';// 注册路由static _initRouter() {// 根页面router.define(root,handler: Handler(handlerFunc: (_, __) => AppMainPage(),),);// 页面A 需要 非对象类型 参数router.define(pageA,handler: Handler(handlerFunc: (_, Map<String, List<String>> params) {// 获取路由参数String? name = params['name']?.first;String? title = params['title']?.first;String? url = params['url']?.first;String? age = params['age']?.first ?? '-1';String? price = params['price']?.first ?? '-1';String? flag = params['flag']?.first ?? 'false';return PageAView(name: name,title: title,url: url,age: int.parse(age),price: double.parse(price),flag: bool.parse(flag));},),);// 页面B 需要 对象类型 参数router.define(pageB,handler: Handler(handlerFunc: (context, Map<String, List<String>> params) {// 获取路由参数TestParamsModel? paramsModel = context?.settings?.arguments as TestParamsModel?;return PageBView(paramsModel: paramsModel);},),);// 页面C 无参数router.define(pageC,handler: Handler(handlerFunc: (_, __) => PageCView(),),);// 页面D 无参数router.define(pageD,handler: Handler(handlerFunc: (_, __) => PageDView(),),);}}
普通无参跳转
NavigatorUtil.push(context, Routers.pageA);
传参跳转 - 非对象类型
/// 传递 非对象参数 方式/// 在path后面,使用 '?' 拼接,再使用 '&' 分割String name = 'jk';/// Invalid argument(s): Illegal percent encoding in URI/// 出现这个异常,说明相关参数,需要转码一下/// 当前举例:中文、链接String title = Uri.encodeComponent('张三');String url = Uri.encodeComponent('https://www.baidu.com');int age = 99;double price = 9.9;bool flag = true;/// 注意:使用 path拼接方式 传递 参数,会改变原来的 路由页面 Path/// path会变成:/pageA?name=jk&title=%E5%BC%A0%E4%B8%89&url=https%3A%2F%2Fwww.baidu.com&age=99&price=9.9&flag=true/// 所以在匹配pageA,找不到,需要还原一下,getOriginalPath(path)NavigatorUtil.push(context,'${Routers.pageA}?name=$name&title=$title&url=$url&age=$age&price=$price&flag=$flag');
传参跳转 - 对象类型
NavigatorUtil.push(context,Routers.pageB,arguments: TestParamsModel(name: 'jk',title: '张三',url: 'https://www.baidu.com',age: 99,price: 9.9,flag: true,)
);
拦截
/// 监听路由栈状态
class PageRouteObserver extends NavigatorObserver {... ...@overridevoid didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {super.didPush(route, previousRoute);/// 当前所在页面 PathString? currentRoutePath = getOriginalPath(previousRoute);/// 要前往的页面 PathString? newRoutePath = getOriginalPath(route);/// 拦截指定页面/// 如果从 PageA 页面,跳转到 PageD,将其拦截if(currentRoutePath == Routers.pageA) {if(newRoutePath == Routers.pageD) {assert((){debugPrint('准备从 PageA页面 进入 pageD页面,进行登陆信息验证');// if(验证不通过) {/// 注意:要延迟一帧WidgetsBinding.instance.addPostFrameCallback((_){// 我这里是pop,视觉上达到无法进入新页面的效果,// 正常业务是跳转到 登陆页面NavigatorUtil.back(navigatorKey.currentContext!);});// }return true;}());}}... ... }... ...}/// 获取原生路径
/// 使用 path拼接方式 传递 参数,会改变原来的 路由页面 Path
///
/// 比如:NavigatorUtil.push(context,'${Routers.pageA}?name=$name&title=$title&url=$url&age=$age&price=$price&flag=$flag');
/// path会变成:/pageA?name=jk&title=%E5%BC%A0%E4%B8%89&url=https%3A%2F%2Fwww.baidu.com&age=99&price=9.9&flag=true
/// 所以再次匹配pageA,找不到,需要还原一下,getOriginalPath(path)
String? getOriginalPath(Route<dynamic>? route) {// 获取原始的路由路径String? fullPath = route?.settings.name;if(fullPath != null) {// 使用正则表达式去除查询参数return fullPath.split('?')[0];}return fullPath;
}
演示效果
全局通知
有几种业务需求,需要在不重启应用的情况下,更新每个页面的数据;
比如 切换主题,什么暗夜模式,还有就是 切换登录 等等,这里我偷了个懒,没有走完整的业务,只是调用当前 已经存在的所有页面的 didChangeDependencies() 方法;
注意:核心代码 我写在 BaseStatefulPageState 里,所以只有 继承 BaseStatefulPage + BaseStatefulPageState 的 页面才能被通知。
具体原理:是 InheritedWidget 的特性,Provider 就是基于它实现的;
从 Flutter 源码看 InheritedWidget 内部实现原理
切换登录
在每个页面的 didChangeDependencies 里处理逻辑,重新请求接口;
@overridevoid didChangeDependencies() {var operate = GlobalOperateProvider.getGlobalOperate(context: context);assert((){debugPrint('HomeView.didChangeDependencies --- $operate');return true;}());// 切换用户// 正常业务流程是:从本地存储,拿到当前最新的用户ID,请求接口,我这里偷了个懒 😄// 直接使用随机数,模拟 不同用户IDif (operate == GlobalOperate.switchLogin) {runSwitchLogin = true;// 重新请求数据// 如果你想刷新的时候,显示loading,加上这个两行viewModel?.pageDataModel?.type = NotifierResultType.loading;viewModel?.pageDataModel?.refreshState();viewModel?.requestData(params: {'curPage': Random().nextInt(20)});}}
这是两个基类的完整代码
import 'package:flutter/material.dart';/// 在执行全局操作后,所有继承 BaseStatefulPageState 的子页面,
/// 都会执行 didChangeDependencies() 方法,然后执行 build() 方法
///
/// 具体原理:是 InheritedWidget 的特性
/// https://loveky.github.io/2018/07/18/how-flutter-inheritedwidget-works//// 全局操作类型
enum GlobalOperate {/// 默认空闲idle,/// 切换登陆switchLogin,/// ... ...
}/// 持有 全局操作状态 的 InheritedWidget
class GlobalNotificationWidget extends InheritedWidget {GlobalNotificationWidget({required this.globalOperate,required super.child});final GlobalOperate globalOperate;static GlobalNotificationWidget? of(BuildContext context) {return context.dependOnInheritedWidgetOfExactType<GlobalNotificationWidget>();}/// 通知所有建立依赖的 子Widget@overridebool updateShouldNotify(covariant GlobalNotificationWidget oldWidget) =>oldWidget.globalOperate != globalOperate &&globalOperate != GlobalOperate.idle;
}/// 具体使用的 全局操作 Widget
///
/// 执行全局操作: GlobalOperateProvider.runGlobalOperate(context: context, operate: GlobalOperate.switchLogin);
/// 获取全局操作类型 GlobalOperateProvider.getGlobalOperate(context: context)
class GlobalOperateProvider extends StatefulWidget {const GlobalOperateProvider({super.key, required this.child});final Widget child;/// 执行全局操作static runGlobalOperate({required BuildContext? context,required GlobalOperate operate,}) {context?.findAncestorStateOfType<_GlobalOperateProviderState>()?._runGlobalOperate(operate: operate);}/// 获取全局操作类型static GlobalOperate? getGlobalOperate({required BuildContext? context}) {return context?.findAncestorStateOfType<_GlobalOperateProviderState>()?.globalOperate;}@overrideState<GlobalOperateProvider> createState() => _GlobalOperateProviderState();
}class _GlobalOperateProviderState extends State<GlobalOperateProvider> {GlobalOperate globalOperate = GlobalOperate.idle;/// 执行全局操作_runGlobalOperate({required GlobalOperate operate}) {// 先重置globalOperate = GlobalOperate.idle;// 再赋值globalOperate = operate;/// 别忘了刷新,如果不刷新,子widget不会执行 didChangeDependencies 方法setState(() {});}@overrideWidget build(BuildContext context) {return GlobalNotificationWidget(globalOperate: globalOperate,child: widget.child,);}
}
演示效果
最好执行完全局操作后,将全局操作状态,重置回 空闲,我是拦截器里面,这个在哪重置,大家随意;
/// Dio拦截器
class DioInterceptor extends InterceptorsWrapper {@overridevoid onRequest(RequestOptions options, RequestInterceptorHandler handler) {... ... /// 重置 全局操作状态if (EnvConfig.isGlobalNotification) {GlobalOperateProvider.runGlobalOperate(context: navigatorKey.currentContext, operate: GlobalOperate.idle);}... ...}}
开发环境配置
我直接创建了三个启动文件;
测试环境
/// 开发环境 入口函数
void main() => Application.runApplication(envTag: EnvTag.develop, // 开发环境platform: ApplicationPlatform.app, // 手机应用baseUrl: 'https://www.wanandroid.com/', // 域名proxyEnable: true, // 是否开启抓包caughtAddress: '192.168.1.3:8888', // 抓包工具的代理地址 + 端口isGlobalNotification: true, // 是否有全局通知操作,比如切换用户/// 异常抛出,会在终端会显示,可帮助开发阶段,快速定位异常所在,/// 但会阻断,后续代码执行,建议 非开发阶段 关闭throwError: false,);
预发布环境
/// 预发布环境 入口函数
void main() => Application.runApplication(envTag: EnvTag.preRelease, // 预发布环境platform: ApplicationPlatform.app, // 手机应用baseUrl: 'https://www.wanandroid.com/', // 域名);
正式环境
/// 正式环境 入口函数
void main() => Application.runApplication(envTag: EnvTag.release, // 正式环境platform: ApplicationPlatform.app, // 手机应用baseUrl: 'https://www.wanandroid.com/', // 域名);
Application
class Application {Application.runApplication({required EnvTag envTag, // 开发环境required String baseUrl, // 域名required ApplicationPlatform platform, // 平台bool proxyEnable = false, // 是否开启抓包String? caughtAddress, // 抓包工具的代理地址 + 端口bool isGlobalNotification = false, // 是否有全局通知操作,比如切换用户bool throwError = false // 异常抛出,会在终端会显示,可帮助开发阶段,快速定位异常所在,但会阻断,后续代码执行}) {EnvConfig.envTag = envTag;EnvConfig.baseUrl = baseUrl;EnvConfig.platform = platform;EnvConfig.proxyEnable = proxyEnable;EnvConfig.caughtAddress = caughtAddress;EnvConfig.isGlobalNotification = isGlobalNotification;EnvConfig.throwError = throwError;/// runZonedGuarded 全局异常监听,实现异常上报runZonedGuarded(() {/// 确保一些依赖,全部初始化WidgetsFlutterBinding.ensureInitialized();/// 监听全局Widget异常,如果发生,将该Widget替换掉ErrorWidget.builder = (FlutterErrorDetails flutterErrorDetails) {return Material(child: Center(child: Text("请联系客服。"),),);};// 初始化路由Routers.configureRouters();// 运行ApprunApp(App());}, (Object error, StackTrace stack) {// 使用第三方服务(例如Sentry)上报错误// Sentry.captureException(error, stackTrace: stackTrace);});}}
网络请求抓包
在Dio里配置的;
注意:如果开启了抓包,但没有启动 抓包工具,Dio 会报 连接异常 DioException [connection error]
/// 代理抓包,测试阶段可能需要void proxy() {if (EnvConfig.proxyEnable) {if (EnvConfig.caughtAddress?.isNotEmpty ?? false) {(httpClientAdapter as IOHttpClientAdapter).createHttpClient = () {final client = HttpClient();client.findProxy = (uri) => 'PROXY ' + EnvConfig.caughtAddress!;client.badCertificateCallback = (cert, host, port) => true;return client;};}}}
演示效果
如何抓包
https://juejin.cn/post/7131928652568231966
https://juejin.cn/post/7035652365826916366
核心基类
Model基类
class BaseModel<VM extends PageViewModel> {VM? vm;void onDispose() {vm = null;}
}
View基类
abstract class BaseStatefulPage<VM extends PageViewModel> extends BaseViewModelStatefulWidget<VM> {BaseStatefulPage({super.key});@overrideBaseStatefulPageState<BaseStatefulPage, VM> createState();
}abstract class BaseStatefulPageState<T extends BaseStatefulPage, VM extends PageViewModel>extends BaseViewModelStatefulWidgetState<T, VM>with AutomaticKeepAliveClientMixin {/// 定义对应的 viewModelVM? viewModel;/// 监听应用生命周期AppLifecycleListener? lifecycleListener;/// 获取应用状态AppLifecycleState? get lifecycleState =>SchedulerBinding.instance.lifecycleState;/// 是否打印 监听应用生命周期的 日志bool debugPrintLifecycleLog = false;/// 进行初始化ViewModel相关操作@overridevoid initState() {super.initState();/// 初始化页面 属性、对象、绑定监听initAttribute();initObserver();/// 初始化ViewModel,并同步生命周期viewModel = viewBindingViewModel();/// 调用viewModel的生命周期,比如 初始化 请求网络数据 等viewModel?.onCreate();/// Flutter 低版本 使用 WidgetsBindingObserver,高版本 使用 AppLifecycleListenerlifecycleListener = AppLifecycleListener(// 监听状态回调onStateChange: onStateChange,// 可见,并且可以响应用户操作时的回调onResume: onResume,// 可见,但无法响应用户操作时的回调onInactive: onInactive,// 隐藏时的回调onHide: onHide,// 显示时的回调onShow: onShow,// 暂停时的回调onPause: onPause,// 暂停后恢复时的回调onRestart: onRestart,// 当退出 并将所有视图与引擎分离时的回调(IOS 支持,Android 不支持)onDetach: onDetach,// 在退出程序时,发出询问的回调(IOS、Android 都不支持)onExitRequested: onExitRequested,);/// 页面布局完成后的回调函数lifecycleListener?.binding.addPostFrameCallback((_) {assert(context != null, 'addPostFrameCallback throw Error context');/// 初始化 需要context 的属性、对象、绑定监听initContextAttribute(context);initContextObserver(context);});}@overridevoid didChangeDependencies() {assert((){debugPrint('BaseStatefulPage.didChangeDependencies --- ${GlobalOperateProvider.getGlobalOperate(context: context)}');return true;}());}/// 监听状态onStateChange(AppLifecycleState state) => mLog('app_state:$state');/// =============================== 根据应用状态的产生的各种回调 ===============================/// 可见,并且可以响应用户操作时的回调/// 比如从应用后台调度到前台时,在 onShow() 后面 执行onResume() => mLog('onResume');/// 可见,但无法响应用户操作时的回调onInactive() => mLog('onInactive');/// 隐藏时的回调onHide() => mLog('onHide');/// 显示时的回调,从应用后台调度到前台时onShow() => mLog('onShow');/// 暂停时的回调onPause() => mLog('onPause');/// 暂停后恢复时的回调onRestart() => mLog('onRestart');/// 这两个回调,不是所有平台都支持,/// 当退出 并将所有视图与引擎分离时的回调(IOS 支持,Android 不支持)onDetach() => mLog('onDetach');/// 在退出程序时,发出询问的回调(IOS、Android 都不支持)/// 响应 [AppExitResponse.exit] 将继续终止,响应 [AppExitResponse.cancel] 将取消终止。Future<AppExitResponse> onExitRequested() async {mLog('onExitRequested');return AppExitResponse.exit;}/// BaseStatefulPageState的子类,重写 dispose()/// 一定要执行父类 dispose(),防止内存泄漏@overridevoid dispose() {/// 销毁顺序/// 1、Model 销毁其持有的 ViewModel/// 2、ViewModel 销毁其持有的 View/// 3、View 销毁其持有的 ViewModel/// 4、销毁监听App生命周期方法if(viewModel?.pageDataModel?.data is BaseModel?) {BaseModel? baseModel = viewModel?.pageDataModel?.data as BaseModel?;baseModel?.onDispose();}if(viewModel?.pageDataModel?.data is BasePagingModel?) {BasePagingModel? basePagingModel = viewModel?.pageDataModel?.data as BasePagingModel?;basePagingModel?.onDispose();}viewModel?.onDispose();viewModel = null;lifecycleListener?.dispose();super.dispose();}/// 是否保持页面状态@overridebool get wantKeepAlive => false;/// View 持有对应的 ViewModelVM viewBindingViewModel();/// 子类重写,初始化 属性、对象/// 这里不是 网络请求操作,而是页面的初始化数据/// 网络请求操作,建议在viewModel.onCreate() 中实现void initAttribute();/// 子类重写,初始化 需要 context 的属性、对象void initContextAttribute(BuildContext context) {}/// 子类重写,初始化绑定监听void initObserver();/// 子类重写,初始化需要 context 的绑定监听void initContextObserver(BuildContext context) {}/// 输出日志void mLog(String info) {if (debugPrintLifecycleLog) {assert(() {debugPrint('--- $info');return true;}());}}/// 手机应用Widget appBuild(BuildContext context) => SizedBox();/// WebWidget webBuild(BuildContext context) => SizedBox();/// PC应用Widget pcBuild(BuildContext context) => SizedBox();@overrideWidget build(BuildContext context) {/// 使用 AutomaticKeepAliveClientMixin 需要 super.build(context);////// 注意:AutomaticKeepAliveClientMixin 只是保存页面状态,并不影响 build 方法执行/// 比如 PageVie的 子页面 使用了AutomaticKeepAliveClientMixin 保存状态,/// PageView切换子页面时,子页面的build的还是会执行if(wantKeepAlive) {super.build(context);}/// 和 GlobalNotificationWidget,建立依赖关系if(EnvConfig.isGlobalNotification) {GlobalNotificationWidget.of(context);}switch (EnvConfig.platform) {case ApplicationPlatform.app: {if (Platform.isAndroid || Platform.isIOS) {// 如果,还想根据当前设备屏幕尺寸细分,// 使用MediaQuery,拿到当前设备信息,进一步适配return appBuild(context);}}case ApplicationPlatform.web: {return webBuild(context);}case ApplicationPlatform.pc: {if(Platform.isWindows || Platform.isMacOS) {return pcBuild(context);}}}return Center(child: Text('当前平台未适配'),);}}
ViewModel基类
/// 基类
abstract class BaseViewModel {}/// 页面继承的ViewModel,不直接使用 BaseViewModel,
/// 是因为BaseViewModel基类里代码,还是不要太多为好,扩展创建新的子类就好
abstract class PageViewModel extends BaseViewModel {/// 定义对应的 viewBaseStatefulPageState? viewState;PageDataModel? pageDataModel;/// 尽量在onCreate方法中编写初始化逻辑void onCreate();/// 对应的widget被销毁了,销毁相关引用对象,避免内存泄漏void onDispose() {viewState = null;pageDataModel = null;}/// 请求数据Future<PageViewModel?> requestData({Map<String, dynamic>? params});}
分页Model基类
/// 内部 有分页列表集合 的实体需要继承 BasePagingModel
class BasePagingModel<VM extends PageViewModel> {int? curPage;List<BasePagingItem>? datas;int? offset;bool? over;int? pageCount;int? size;int? total;VM? vm;BasePagingModel({this.curPage, this.datas, this.offset, this.over,this.pageCount, this.size, this.total});void onDispose() {vm = null;}
}/// 是分页列表 集合子项 实体需要继承 BasePagingItem
class BasePagingItem {}
分页处理核心类
/// 分页数据相关/// 分页行为:下拉刷新/上拉加载更多
enum PagingBehavior {/// 空闲,默认状态idle,/// 加载load,/// 刷新refresh;
}/// 分页状态:执行完 下拉刷新/上拉加载更多后,得到的状态
enum PagingState {/// 空闲,默认状态idle,/// 加载成功loadSuccess,/// 加载失败loadFail,/// 没有更多数据了loadNoData,/// 正在加载curLoading,/// 刷新成功refreshSuccess,/// 刷新失败refreshFail,/// 正在刷新curRefreshing,
}/// 分页数据对象
class PagingDataModel<DM extends BaseChangeNotifier, VM extends PageViewModel> {// 当前页码int curPage;// 总共多少页int pageCount;// 总共 数据数量int total;// 当前页 数据数量int size;// 完整的数据dynamic data;// 分页参数 字段,一般情况都是固定的,以防万一String? curPageField;// 数据列表List<dynamic> listData = [];// 当前的PageDataModelDM? pageDataModel;// 当前的PageViewModelVM? pageViewModel;PagingBehavior pagingBehavior = PagingBehavior.idle;PagingState pagingState = PagingState.idle;PagingDataModel({this.curPage = 0,this.pageCount = 0,this.total = 0,this.size = 0,this.data,this.curPageField = 'curPage',this.pageDataModel}) : listData = [];/// 这两个方法,由 RefreshLoadWidget 组件调用/// 加载更多,追加数据Future<PagingState> loadListData() async {PagingState pagingState = PagingState.curLoading;pagingBehavior = PagingBehavior.load;Map<String, dynamic>? param = {curPageField!: curPage++};PageViewModel? currentPageViewModel = await pageViewModel?.requestData(params: param);if(currentPageViewModel?.pageDataModel?.type == NotifierResultType.success) {// 没有更多数据了if(currentPageViewModel?.pageDataModel?.total == listData.length) {pagingState = PagingState.loadNoData;} else {pagingState = PagingState.loadSuccess;}} else {pagingState = PagingState.loadFail;}return pagingState;}/// 下拉刷新数据Future<PagingState> refreshListData() async {PagingState pagingState = PagingState.curRefreshing;pagingBehavior = PagingBehavior.refresh;curPage = 0;Map<String, dynamic>? param = {curPageField!: curPage};PageViewModel? currentPageViewModel = await pageViewModel?.requestData(params: param);if(currentPageViewModel?.pageDataModel?.type == NotifierResultType.success) {pagingState = PagingState.refreshSuccess;} else {pagingState = PagingState.refreshFail;}return pagingState;}}
源码地址
GitHub - LanSeLianMa/flutter_develop_template: Flutter项目开发模版,开箱即用
参考文档
Dio:https://juejin.cn/post/7360227158662807589
路由:Flutter中封装Fluro路由配置,以及无context跳转与传参 - 掘金
MVVM:https://juejin.cn/post/7166503123983269901
API
玩Android平台的开放 API;
玩Android 开放API-玩Android - wanandroid.com