当前案例 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: false
- prefer_const_constructors: false
- package_names: null
修改前

修改后

持有关系:
View持有 ViewModel;
Model持有ViewModel;
ViewModel持有View;
ViewModel持有Model;
注意:这种持有关系,有很高的内存泄漏风险,所以我在基类的 dispose() 中进行了销毁,子类重写一定要调用 super.dispose();
- /// BaseStatefulPageState的子类,重写 dispose()
- /// 一定要执行父类 dispose(),防止内存泄漏
- @override
- void dispose() {
- /// 销毁顺序
-
- /// 1、Model 销毁其持有的 ViewModel
- 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();
- }
-
- /// 2、ViewModel 销毁其持有的 View
- /// 3、ViewModel 销毁其持有的 Model
- viewModel?.onDispose();
-
- /// 4、View 销毁其持有的 ViewModel
- viewModel = null;
-
- /// 5、销毁监听App生命周期方法
- lifecycleListener?.dispose();
- super.dispose();
- }
基类放在文章最后说,这里先忽略;
- class HomeListModel extends BaseModel {
-
- ... ...
-
- ValueNotifier<int> tapNum = ValueNotifier<int>(0); // 点击次数
-
- @override
- void onDispose() {
- tapNum.dispose();
- super.onDispose();
- }
-
- ... ...
-
- }
-
- ... ...
- class HomeView extends BaseStatefulPage<HomeViewModel> {
- HomeView({super.key});
-
- @override
- HomeViewState createState() => HomeViewState();
- }
-
- class HomeViewState extends BaseStatefulPageState<HomeView, HomeViewModel> {
-
- @override
- HomeViewModel viewBindingViewModel() {
-
- /// ViewModel 和 View 相互持有
- return HomeViewModel()..viewState = this;
-
- }
-
- /// 初始化 页面 属性
- @override
- void initAttribute() {
- ... ...
- }
-
- /// 初始化 页面 相关对象绑定
- @override
- void initObserver() {
- ... ...
- }
-
- @override
- void dispose() {
- ... ...
-
- /// BaseStatefulPageState的子类,重写 dispose()
- /// 一定要执行父类 dispose(),防止内存泄漏
- super.dispose();
- }
-
- ValueNotifier<int> tapNum = ValueNotifier<int>(0);
-
- @override
- Widget appBuild(BuildContext context) {
-
- ... ...
-
- }
-
- /// 是否保存页面状态
- @override
- bool get wantKeepAlive => true;
-
- }
- class HomeViewModel extends PageViewModel<HomeViewState> {
-
- HomeViewState? state;
-
- @override
- onCreate() {
-
- /// 拿到 页面状态里的 对象、属性 等等
- debugPrint('---runSwitchLogin:${state.runSwitchLogin}');
- ... ...
-
- /// 初始化 网络请求
- requestData();
- }
-
- @override
- onDispose() {
- ... ...
-
- /// 别忘了执行父类的 onDispose
- super.onDispose();
- }
-
- /// 请求数据
- @override
- Future
requestData({Map<String, dynamic>? params}) async { -
- ... ...
-
- }
- }
- class HomeRepository {
-
- /// 获取首页数据
- Future
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.toString();
- }
-
- return pageViewModel;
- }
-
- }
- class PersonalRepository {
-
- /// 注册
- Future
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.toString();
- }
-
- return pageViewModel;
- }
-
- /// 登陆
- Future
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.toString();
- }
-
- return pageViewModel;
- }
-
- }
- class MessageRepository {
-
- /// 分页列表
- Future
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.toString();
- }
-
- return pageViewModel;
- }
-
- }
剩下的 ResultFul API 风格请求,我就不一一演示了,DioClient 里都封装好了,照葫芦画瓢就好。
ResultFul API 风格
GET:从服务器获取一项或者多项数据
POST:在服务器新建一个资源
PUT:在服务器更新所有资源
PATCH:更新部分属性
DELETE:从服务器删除资源
这个组件是我封装的,和 ViewModel 里的 PageDataModel 绑定,当PageDataModel里的数据发生改变,就可以通知 NotifierPageWidget 刷新;
- enum NotifierResultType {
- // 不检查
- notCheck,
-
- // 加载中
- loading,
-
- // 请求成功
- success,
-
- // 这种属于请求成功,但业务不通过,比如没有权限
- unauthorized,
-
- // 请求异常
- dioError,
-
- // 未知异常
- fail,
- }
-
- typedef NotifierPageWidgetBuilder
extends BaseChangeNotifier> = Widget - Function(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
createState() => _NotifierPageWidgetState(); - }
-
- class _NotifierPageWidgetState<T extends BaseChangeNotifier>
- extends State<NotifierPageWidget<T>> {
- PageDataModel? model;
-
- /// 刷新UI
- refreshUI() => setState(() {
- model = widget.model;
- });
-
- /// 对数据进行绑定监听
- @override
- void initState() {
- super.initState();
-
- model = widget.model;
-
- // 先清空一次已注册的Listener,防止重复触发
- model?.removeListener(refreshUI);
-
- // 添加监听
- model?.addListener(refreshUI);
- }
-
- @override
- void didUpdateWidget(covariant NotifierPageWidget
oldWidget) { - super.didUpdateWidget(oldWidget);
- if (oldWidget.model != widget.model) {
- // 先清空一次已注册的Listener,防止重复触发
- oldWidget.model?.removeListener(refreshUI);
-
- model = widget.model;
-
- // 添加监听
- model?.addListener(refreshUI);
- }
- }
-
- @override
- Widget 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
?; - 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}'),
- );
- }
-
- @override
- void dispose() {
- widget.model?.removeListener(refreshUI);
- super.dispose();
- }
- }
使用
- class HomeView extends BaseStatefulPage<HomeViewModel> {
- HomeView({super.key});
-
- @override
- HomeViewState createState() => HomeViewState();
- }
-
- class HomeViewState extends BaseStatefulPageState<HomeView, HomeViewModel> {
-
- @override
- Widget appBuild(BuildContext context) {
-
- return Scaffold(
- ... ...
-
- body: NotifierPageWidget
( - 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}'),
- );
- }),
-
- ... ...
-
- ],
- );
- }
- ),
- );
- }
-
- }
这个就是Flutter自带的组件,配合ValueNotifier使用,我主要用它做局部刷新;
- class HomeView extends BaseStatefulPage<HomeViewModel> {
- HomeView({super.key});
-
- @override
- HomeViewState createState() => HomeViewState();
- }
-
- class HomeViewState extends BaseStatefulPageState<HomeView, HomeViewModel> {
-
- ... ...
-
- ValueNotifier<int> tapNum = ValueNotifier<int>(0);
-
- @override
- Widget 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 = '/';
-
- // 页面A
- static String pageA = '/pageA';
-
- // 页面B
- static String pageB = '/pageB';
-
- // 页面C
- static String pageC = '/pageC';
-
- // 页面D
- static 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 {
- ... ...
-
- @override
- void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
- super.didPush(route, previousRoute);
-
- /// 当前所在页面 Path
- String? currentRoutePath = getOriginalPath(previousRoute);
-
- /// 要前往的页面 Path
- String? 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 里处理逻辑,重新请求接口;
- @override
- void didChangeDependencies() {
- var operate = GlobalOperateProvider.getGlobalOperate(context: context);
-
- assert((){
- debugPrint('HomeView.didChangeDependencies --- $operate');
- return true;
- }());
-
- // 切换用户
- // 正常业务流程是:从本地存储,拿到当前最新的用户ID,请求接口,我这里偷了个懒 😄
- // 直接使用随机数,模拟 不同用户ID
- if (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
(); - }
-
- /// 通知所有建立依赖的 子Widget
- @override
- bool 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;
- }
-
- @override
- State
createState() => _GlobalOperateProviderState(); - }
-
- class _GlobalOperateProviderState extends State<GlobalOperateProvider> {
- GlobalOperate globalOperate = GlobalOperate.idle;
-
- /// 执行全局操作
- _runGlobalOperate({required GlobalOperate operate}) {
- // 先重置
- globalOperate = GlobalOperate.idle;
-
- // 再赋值
- globalOperate = operate;
-
- /// 别忘了刷新,如果不刷新,子widget不会执行 didChangeDependencies 方法
- setState(() {});
- }
-
- @override
- Widget build(BuildContext context) {
- return GlobalNotificationWidget(
- globalOperate: globalOperate,
- child: widget.child,
- );
- }
- }

最好执行完全局操作后,将全局操作状态,重置回 空闲,我是拦截器里面,这个在哪重置,大家随意;
- /// Dio拦截器
- class DioInterceptor extends InterceptorsWrapper {
-
- @override
- void 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();
-
- // 运行App
- runApp(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
- class BaseModel<VM extends PageViewModel> {
-
- VM? vm;
-
- void onDispose() {
- vm = null;
- }
- }
- abstract class BaseStatefulPage<VM extends PageViewModel> extends BaseViewModelStatefulWidget<VM> {
- BaseStatefulPage({super.key});
-
- @override
- BaseStatefulPageState
createState(); - }
-
- abstract class BaseStatefulPageState<T extends BaseStatefulPage, VM extends PageViewModel>
- extends BaseViewModelStatefulWidgetState<T, VM>
- with AutomaticKeepAliveClientMixin {
-
- /// 定义对应的 viewModel
- VM? viewModel;
-
- /// 监听应用生命周期
- AppLifecycleListener? lifecycleListener;
-
- /// 获取应用状态
- AppLifecycleState? get lifecycleState =>
- SchedulerBinding.instance.lifecycleState;
-
- /// 是否打印 监听应用生命周期的 日志
- bool debugPrintLifecycleLog = false;
-
- /// 进行初始化ViewModel相关操作
- @override
- void initState() {
- super.initState();
-
- /// 初始化页面 属性、对象、绑定监听
- initAttribute();
- initObserver();
-
- /// 初始化ViewModel,并同步生命周期
- viewModel = viewBindingViewModel();
-
- /// 调用viewModel的生命周期,比如 初始化 请求网络数据 等
- viewModel?.onCreate();
-
- /// Flutter 低版本 使用 WidgetsBindingObserver,高版本 使用 AppLifecycleListener
- lifecycleListener = 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);
- });
- }
-
- @override
- void 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
onExitRequested() async { - mLog('onExitRequested');
- return AppExitResponse.exit;
- }
-
- /// BaseStatefulPageState的子类,重写 dispose()
- /// 一定要执行父类 dispose(),防止内存泄漏
- @override
- void dispose() {
- /// 销毁顺序
-
- /// 1、Model 销毁其持有的 ViewModel
- 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();
- }
-
- /// 2、ViewModel 销毁其持有的 View
- /// 3、ViewModel 销毁其持有的 Model
- viewModel?.onDispose();
-
- /// 4、View 销毁其持有的 ViewModel
- viewModel = null;
-
- /// 5、销毁监听App生命周期方法
- lifecycleListener?.dispose();
- super.dispose();
- }
-
- /// 是否保持页面状态
- @override
- bool get wantKeepAlive => false;
-
- /// View 持有对应的 ViewModel
- VM 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();
-
- /// Web
- Widget webBuild(BuildContext context) => SizedBox();
-
- /// PC应用
- Widget pcBuild(BuildContext context) => SizedBox();
-
- @override
- Widget 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('当前平台未适配'),
- );
- }
-
- }
- /// 基类
- abstract class BaseViewModel {
-
- }
-
- /// 页面继承的ViewModel,不直接使用 BaseViewModel,
- /// 是因为BaseViewModel基类里代码,还是不要太多为好,扩展创建新的子类就好
- abstract class PageViewModel<T extends State> extends BaseViewModel {
-
- /// 定义对应的 view
- BaseStatefulPageState? viewState;
-
- /// 使用 直接拿它
- T get state => viewState as T;
-
- /// 页面数据model
- PageDataModel? pageDataModel;
-
- /// 尽量在onCreate方法中编写初始化逻辑
- void onCreate();
-
- /// 对应的widget被销毁了,销毁相关引用对象,避免内存泄漏
- void onDispose() {
- viewState = null;
- pageDataModel = null;
- }
-
- /// 请求数据
- Future
requestData({Map<String, dynamic>? params}); -
- }
- /// 内部 有分页列表集合 的实体需要继承 BasePagingModel
- class BasePagingModel<VM extends PageViewModel> {
- int? curPage;
- List
? 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;
-
- /// 响应的完整数据
- /// 你可能还需要,响应数据的 其他字段,
- ///
- /// assert((){
- /// var mListModel = pageDataModel?.data as MessageListModel?; // 转化成对应的Model
- /// debugPrint('---pageCount:${mListModel?.pageCount}'); // 获取字段
- /// return true;
- /// }());
- dynamic data;
-
- // 分页参数 字段,一般情况都是固定的,以防万一
- String? curPageField;
-
- // 数据列表
- List<dynamic> listData = [];
-
- // 当前的PageDataModel
- DM? pageDataModel;
-
- // 当前的PageViewModel
- VM? 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
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
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
玩Android平台的开放 API;
玩Android 开放API-玩Android - wanandroid.com