• Flutter项目开发模版,开箱即用


    前言

    当前案例 Flutter SDK版本:3.22.2

    每当我们开始一个新项目,都会 引入常用库、封装工具类,配置环境等等,我参考了一些文档,将这些内容整合、简单修改、二次封装,得到了一个开箱即用的Flutter开发模版,即使看不懂封装的工具对象原理,也没关系,模版化的使用方式,小白也可以快速开发Flutter项目。

    快速上手

    用到的依赖库

    1. dio: ^5.4.3+1 // 网络请求
    2. fluro: ^2.0.5 // 路由
    3. pull_to_refresh: ^2.0.0 // 下拉刷新 / 上拉加载更多

    修改规则

    默认使用的是Flutter团队制定的规则,但每个开发团队规则都不一样,违反规则的地方会出现黄色波浪下划线,比如我定义常量喜欢字母全部大写,这和默认规则不符;

    修改 Flutter项目里的 analysis_options.yaml 文件,找到 rules,添加以下配置;

    1. rules:
    2. use_key_in_widget_constructors: false
    3. prefer_const_constructors: false
    4. package_names: null

     修改前

    修改后 

    MVVM

    • MVVM 设计模式,相信大家应该不陌生,我简单说一下每层主要负责做什么;
    • Model: 数据相关操作;
    • View:UI相关操作;
    • ViewModel:业务逻辑相关操作。

    持有关系:

    View持有 ViewModel;

    Model持有ViewModel;

    ViewModel持有View;

    ViewModel持有Model;

    注意:这种持有关系,有很高的内存泄漏风险,所以我在基类的 dispose() 中进行了销毁子类重写一定要调用 super.dispose()

    1. /// BaseStatefulPageState的子类,重写 dispose()
    2. /// 一定要执行父类 dispose(),防止内存泄漏
    3. @override
    4. void dispose() {
    5. /// 销毁顺序
    6. /// 1、Model 销毁其持有的 ViewModel
    7. if(viewModel?.pageDataModel?.data is BaseModel?) {
    8. BaseModel? baseModel = viewModel?.pageDataModel?.data as BaseModel?;
    9. baseModel?.onDispose();
    10. }
    11. if(viewModel?.pageDataModel?.data is BasePagingModel?) {
    12. BasePagingModel? basePagingModel = viewModel?.pageDataModel?.data as BasePagingModel?;
    13. basePagingModel?.onDispose();
    14. }
    15. /// 2、ViewModel 销毁其持有的 View
    16. /// 3、ViewModel 销毁其持有的 Model
    17. viewModel?.onDispose();
    18. /// 4、View 销毁其持有的 ViewModel
    19. viewModel = null;
    20. /// 5、销毁监听App生命周期方法
    21. lifecycleListener?.dispose();
    22. super.dispose();
    23. }

    基类放在文章最后说,这里先忽略;

    Model

    1. class HomeListModel extends BaseModel {
    2. ... ...
    3. ValueNotifier<int> tapNum = ValueNotifier<int>(0); // 点击次数
    4. @override
    5. void onDispose() {
    6. tapNum.dispose();
    7. super.onDispose();
    8. }
    9. ... ...
    10. }
    11. ... ...

    View

    1. class HomeView extends BaseStatefulPage<HomeViewModel> {
    2. HomeView({super.key});
    3. @override
    4. HomeViewState createState() => HomeViewState();
    5. }
    6. class HomeViewState extends BaseStatefulPageState<HomeView, HomeViewModel> {
    7. @override
    8. HomeViewModel viewBindingViewModel() {
    9. /// ViewModel 和 View 相互持有
    10. return HomeViewModel()..viewState = this;
    11. }
    12. /// 初始化 页面 属性
    13. @override
    14. void initAttribute() {
    15. ... ...
    16. }
    17. /// 初始化 页面 相关对象绑定
    18. @override
    19. void initObserver() {
    20. ... ...
    21. }
    22. @override
    23. void dispose() {
    24. ... ...
    25. /// BaseStatefulPageState的子类,重写 dispose()
    26. /// 一定要执行父类 dispose(),防止内存泄漏
    27. super.dispose();
    28. }
    29. ValueNotifier<int> tapNum = ValueNotifier<int>(0);
    30. @override
    31. Widget appBuild(BuildContext context) {
    32. ... ...
    33. }
    34. /// 是否保存页面状态
    35. @override
    36. bool get wantKeepAlive => true;
    37. }

    ViewModel

    1. class HomeViewModel extends PageViewModel<HomeViewState> {
    2. HomeViewState? state;
    3. @override
    4. onCreate() {
    5. /// 拿到 页面状态里的 对象、属性 等等
    6. debugPrint('---runSwitchLogin:${state.runSwitchLogin}');
    7. ... ...
    8. /// 初始化 网络请求
    9. requestData();
    10. }
    11. @override
    12. onDispose() {
    13. ... ...
    14. /// 别忘了执行父类的 onDispose
    15. super.onDispose();
    16. }
    17. /// 请求数据
    18. @override
    19. Future requestData({Map<String, dynamic>? params}) async {
    20. ... ...
    21. }
    22. }

    网络请求

    Get请求

    1. class HomeRepository {
    2. /// 获取首页数据
    3. Future getHomeData({
    4. required PageViewModel pageViewModel,
    5. CancelToken? cancelToken,
    6. int curPage = 0,
    7. }) async {
    8. try {
    9. Response response = await DioClient().doGet('project/list/$curPage/json?cid=294', cancelToken: cancelToken);
    10. if(response.statusCode == REQUEST_SUCCESS) {
    11. /// 请求成功
    12. pageViewModel.pageDataModel?.type = NotifierResultType.success;
    13. /// ViewModel 和 Model 相互持有
    14. HomeListModel model = HomeListModel.fromJson(response.data);
    15. model.vm = pageViewModel;
    16. pageViewModel.pageDataModel?.data = model;
    17. } else {
    18. /// 请求成功,但业务不通过,比如没有权限
    19. pageViewModel.pageDataModel?.type = NotifierResultType.unauthorized;
    20. pageViewModel.pageDataModel?.errorMsg = response.statusMessage;
    21. }
    22. } on DioException catch (dioEx) {
    23. /// 请求异常
    24. pageViewModel.pageDataModel?.type = NotifierResultType.dioError;
    25. pageViewModel.pageDataModel?.errorMsg = dioErrorConversionText(dioEx);
    26. } catch (e) {
    27. /// 未知异常
    28. pageViewModel.pageDataModel?.type = NotifierResultType.fail;
    29. pageViewModel.pageDataModel?.errorMsg = e.toString();
    30. }
    31. return pageViewModel;
    32. }
    33. }

    Post请求

    1. class PersonalRepository {
    2. /// 注册
    3. Future registerUser({
    4. required PageViewModel pageViewModel,
    5. Map<String, dynamic>? params,
    6. CancelToken? cancelToken,
    7. }) async {
    8. try {
    9. Response response = await DioClient().doPost(
    10. 'user/register',
    11. params: params,
    12. cancelToken: cancelToken,
    13. );
    14. if(response.statusCode == REQUEST_SUCCESS) {
    15. /// 请求成功
    16. pageViewModel.pageDataModel?.type = NotifierResultType.success; // 请求成功
    17. /// ViewModel 和 Model 相互持有
    18. UserInfoModel model = UserInfoModel.fromJson(response.data)..isLogin = false;
    19. model.vm = pageViewModel;
    20. pageViewModel.pageDataModel?.data = model;
    21. } else {
    22. /// 请求成功,但业务不通过,比如没有权限
    23. pageViewModel.pageDataModel?.type = NotifierResultType.unauthorized;
    24. pageViewModel.pageDataModel?.errorMsg = response.statusMessage;
    25. }
    26. } on DioException catch (dioEx) {
    27. /// 请求异常
    28. pageViewModel.pageDataModel?.type = NotifierResultType.dioError;
    29. pageViewModel.pageDataModel?.errorMsg = dioErrorConversionText(dioEx);
    30. } catch (e) {
    31. /// 未知异常
    32. pageViewModel.pageDataModel?.type = NotifierResultType.fail;
    33. pageViewModel.pageDataModel?.errorMsg = e.toString();
    34. }
    35. return pageViewModel;
    36. }
    37. /// 登陆
    38. Future loginUser({
    39. required PageViewModel pageViewModel,
    40. Map<String, dynamic>? params,
    41. CancelToken? cancelToken,
    42. }) async {
    43. try {
    44. Response response = await DioClient().doPost(
    45. 'user/login',
    46. params: params,
    47. cancelToken: cancelToken,
    48. );
    49. if(response.statusCode == REQUEST_SUCCESS) {
    50. /// 请求成功
    51. pageViewModel.pageDataModel?.type = NotifierResultType.success;
    52. /// ViewModel 和 Model 相互持有
    53. UserInfoModel model = UserInfoModel.fromJson(response.data)..isLogin = true;
    54. model.vm = pageViewModel;
    55. pageViewModel.pageDataModel?.data = model;
    56. } else {
    57. /// 请求成功,但业务不通过,比如没有权限
    58. pageViewModel.pageDataModel?.type = NotifierResultType.unauthorized;
    59. pageViewModel.pageDataModel?.errorMsg = response.statusMessage;
    60. }
    61. } on DioException catch (dioEx) {
    62. /// 请求异常
    63. pageViewModel.pageDataModel?.type = NotifierResultType.dioError;
    64. pageViewModel.pageDataModel?.errorMsg = dioErrorConversionText(dioEx);
    65. } catch (e) {
    66. /// 未知异常
    67. pageViewModel.pageDataModel?.type = NotifierResultType.fail;
    68. pageViewModel.pageDataModel?.errorMsg = e.toString();
    69. }
    70. return pageViewModel;
    71. }
    72. }

    分页数据请求

    1. class MessageRepository {
    2. /// 分页列表
    3. Future getMessageData({
    4. required PageViewModel pageViewModel,
    5. CancelToken? cancelToken,
    6. int curPage = 0,
    7. }) async {
    8. try {
    9. Response response = await DioClient().doGet('article/list/$curPage/json', cancelToken: cancelToken);
    10. if(response.statusCode == REQUEST_SUCCESS) {
    11. /// 请求成功
    12. pageViewModel.pageDataModel?.type = NotifierResultType.success;
    13. /// 有分页
    14. pageViewModel.pageDataModel?.isPaging = true;
    15. /// 分页代码
    16. pageViewModel.pageDataModel?.correlationPaging(pageViewModel, MessageListModel.fromJson(response.data));
    17. } else {
    18. /// 请求成功,但业务不通过,比如没有权限
    19. pageViewModel.pageDataModel?.type = NotifierResultType.unauthorized;
    20. pageViewModel.pageDataModel?.errorMsg = response.statusMessage;
    21. }
    22. } on DioException catch (dioEx) {
    23. /// 请求异常
    24. pageViewModel.pageDataModel?.type = NotifierResultType.dioError;
    25. pageViewModel.pageDataModel?.errorMsg = dioErrorConversionText(dioEx);
    26. } catch (e) {
    27. /// 未知异常
    28. pageViewModel.pageDataModel?.type = NotifierResultType.fail;
    29. pageViewModel.pageDataModel?.errorMsg = e.toString();
    30. }
    31. return pageViewModel;
    32. }
    33. }

    剩下的 ResultFul API 风格请求,我就不一一演示了,DioClient 里都封装好了,照葫芦画瓢就好。

    ResultFul API 风格
    GET:从服务器获取一项或者多项数据
    POST:在服务器新建一个资源
    PUT:在服务器更新所有资源
    PATCH:更新部分属性
    DELETE:从服务器删除资源

    刷新页面

    NotifierPageWidget

    这个组件是我封装的,和 ViewModel 里的 PageDataModel 绑定,当PageDataModel里的数据发生改变,就可以通知 NotifierPageWidget 刷新;

    1. enum NotifierResultType {
    2. // 不检查
    3. notCheck,
    4. // 加载中
    5. loading,
    6. // 请求成功
    7. success,
    8. // 这种属于请求成功,但业务不通过,比如没有权限
    9. unauthorized,
    10. // 请求异常
    11. dioError,
    12. // 未知异常
    13. fail,
    14. }
    15. typedef NotifierPageWidgetBuilderextends BaseChangeNotifier> = Widget
    16. Function(BuildContext context, PageDataModel model);
    17. /// 这个是配合 PageDataModel 类使用的
    18. class NotifierPageWidget<T extends BaseChangeNotifier> extends StatefulWidget {
    19. NotifierPageWidget({
    20. super.key,
    21. required this.model,
    22. required this.builder,
    23. });
    24. /// 需要监听的数据观察类
    25. final PageDataModel? model;
    26. final NotifierPageWidgetBuilder builder;
    27. @override
    28. _NotifierPageWidgetState createState() => _NotifierPageWidgetState();
    29. }
    30. class _NotifierPageWidgetState<T extends BaseChangeNotifier>
    31. extends State<NotifierPageWidget<T>> {
    32. PageDataModel? model;
    33. /// 刷新UI
    34. refreshUI() => setState(() {
    35. model = widget.model;
    36. });
    37. /// 对数据进行绑定监听
    38. @override
    39. void initState() {
    40. super.initState();
    41. model = widget.model;
    42. // 先清空一次已注册的Listener,防止重复触发
    43. model?.removeListener(refreshUI);
    44. // 添加监听
    45. model?.addListener(refreshUI);
    46. }
    47. @override
    48. void didUpdateWidget(covariant NotifierPageWidget oldWidget) {
    49. super.didUpdateWidget(oldWidget);
    50. if (oldWidget.model != widget.model) {
    51. // 先清空一次已注册的Listener,防止重复触发
    52. oldWidget.model?.removeListener(refreshUI);
    53. model = widget.model;
    54. // 添加监听
    55. model?.addListener(refreshUI);
    56. }
    57. }
    58. @override
    59. Widget build(BuildContext context) {
    60. if (model?.type == NotifierResultType.notCheck) {
    61. return widget.builder(context, model!);
    62. }
    63. if (model?.type == NotifierResultType.loading) {
    64. return Center(
    65. child: Text('加载中...'),
    66. );
    67. }
    68. if (model?.type == NotifierResultType.success) {
    69. if (model?.data == null) {
    70. return Center(
    71. child: Text('数据为空'),
    72. );
    73. }
    74. if(model?.isPaging ?? false) {
    75. var lists = model?.data?.datas as List?;
    76. if(lists?.isEmpty ?? false){
    77. return Center(
    78. child: Text('列表数据为空'),
    79. );
    80. };
    81. }
    82. return widget.builder(context, model!);
    83. }
    84. if (model?.type == NotifierResultType.unauthorized) {
    85. return Center(
    86. child: Text('业务不通过:${model?.errorMsg}'),
    87. );
    88. }
    89. /// 异常抛出,会在终端会显示,可帮助开发阶段,快速定位异常所在,
    90. /// 但会阻断,后续代码执行,建议 非开发阶段 关闭
    91. if(EnvConfig.throwError) {
    92. throw Exception('${model?.errorMsg}');
    93. }
    94. if (model?.type == NotifierResultType.dioError) {
    95. return Center(
    96. child: Text('dioError异常:${model?.errorMsg}'),
    97. );
    98. }
    99. if (model?.type == NotifierResultType.fail) {
    100. return Center(
    101. child: Text('未知异常:${model?.errorMsg}'),
    102. );
    103. }
    104. return Center(
    105. child: Text('请联系客服:${model?.errorMsg}'),
    106. );
    107. }
    108. @override
    109. void dispose() {
    110. widget.model?.removeListener(refreshUI);
    111. super.dispose();
    112. }
    113. }

    使用 

    1. class HomeView extends BaseStatefulPage<HomeViewModel> {
    2. HomeView({super.key});
    3. @override
    4. HomeViewState createState() => HomeViewState();
    5. }
    6. class HomeViewState extends BaseStatefulPageState<HomeView, HomeViewModel> {
    7. @override
    8. Widget appBuild(BuildContext context) {
    9. return Scaffold(
    10. ... ...
    11. body: NotifierPageWidget(
    12. model: viewModel?.pageDataModel,
    13. builder: (context, dataModel) {
    14. final data = dataModel.data as HomeListModel?;
    15. ... ...
    16. return Stack(
    17. children: [
    18. ListView.builder(
    19. padding: EdgeInsets.zero,
    20. itemCount: data?.datas?.length ?? 0,
    21. itemBuilder: (context, index) {
    22. return Container(
    23. width: MediaQuery.of(context).size.width,
    24. height: 50,
    25. alignment: Alignment.center,
    26. child: Text('${data?.datas?[index].title}'),
    27. );
    28. }),
    29. ... ...
    30. ],
    31. );
    32. }
    33. ),
    34. );
    35. }
    36. }

    ValueListenableBuilder

    这个就是Flutter自带的组件配合ValueNotifier使用,我主要用它做局部刷新

    1. class HomeView extends BaseStatefulPage<HomeViewModel> {
    2. HomeView({super.key});
    3. @override
    4. HomeViewState createState() => HomeViewState();
    5. }
    6. class HomeViewState extends BaseStatefulPageState<HomeView, HomeViewModel> {
    7. ... ...
    8. ValueNotifier<int> tapNum = ValueNotifier<int>(0);
    9. @override
    10. Widget appBuild(BuildContext context) {
    11. return Scaffold(
    12. appBar: AppBar(
    13. backgroundColor: AppBarTheme.of(context).backgroundColor,
    14. /// 局部刷新
    15. title: ValueListenableBuilder<int>(
    16. valueListenable: tapNum,
    17. builder: (context, value, _) {
    18. return Text(
    19. 'Home:$value',
    20. style: TextStyle(fontSize: 20),
    21. );
    22. },
    23. ),
    24. ... ...
    25. ),
    26. );
    27. }
    28. }

    演示效果

    路由

    配置

    1. class Routers {
    2. static FluroRouter router = FluroRouter();
    3. // 配置路由
    4. static void configureRouters() {
    5. router.notFoundHandler = Handler(handlerFunc: (_, __) {
    6. // 找不到路由时,返回指定提示页面
    7. return Scaffold(
    8. body: const Center(
    9. child: Text('404'),
    10. ),
    11. );
    12. });
    13. // 初始化路由
    14. _initRouter();
    15. }
    16. // 设置页面
    17. // 页面标识
    18. static String root = '/';
    19. // 页面A
    20. static String pageA = '/pageA';
    21. // 页面B
    22. static String pageB = '/pageB';
    23. // 页面C
    24. static String pageC = '/pageC';
    25. // 页面D
    26. static String pageD = '/pageD';
    27. // 注册路由
    28. static _initRouter() {
    29. // 根页面
    30. router.define(
    31. root,
    32. handler: Handler(
    33. handlerFunc: (_, __) => AppMainPage(),
    34. ),
    35. );
    36. // 页面A 需要 非对象类型 参数
    37. router.define(
    38. pageA,
    39. handler: Handler(
    40. handlerFunc: (_, Map<String, List<String>> params) {
    41. // 获取路由参数
    42. String? name = params['name']?.first;
    43. String? title = params['title']?.first;
    44. String? url = params['url']?.first;
    45. String? age = params['age']?.first ?? '-1';
    46. String? price = params['price']?.first ?? '-1';
    47. String? flag = params['flag']?.first ?? 'false';
    48. return PageAView(
    49. name: name,
    50. title: title,
    51. url: url,
    52. age: int.parse(age),
    53. price: double.parse(price),
    54. flag: bool.parse(flag)
    55. );
    56. },
    57. ),
    58. );
    59. // 页面B 需要 对象类型 参数
    60. router.define(
    61. pageB,
    62. handler: Handler(
    63. handlerFunc: (context, Map<String, List<String>> params) {
    64. // 获取路由参数
    65. TestParamsModel? paramsModel = context?.settings?.arguments as TestParamsModel?;
    66. return PageBView(paramsModel: paramsModel);
    67. },
    68. ),
    69. );
    70. // 页面C 无参数
    71. router.define(
    72. pageC,
    73. handler: Handler(
    74. handlerFunc: (_, __) => PageCView(),
    75. ),
    76. );
    77. // 页面D 无参数
    78. router.define(
    79. pageD,
    80. handler: Handler(
    81. handlerFunc: (_, __) => PageDView(),
    82. ),
    83. );
    84. }
    85. }

    普通无参跳转

    NavigatorUtil.push(context, Routers.pageA);

    传参跳转 - 非对象类型

    1. /// 传递 非对象参数 方式
    2. /// 在path后面,使用 '?' 拼接,再使用 '&' 分割
    3. String name = 'jk';
    4. /// Invalid argument(s): Illegal percent encoding in URI
    5. /// 出现这个异常,说明相关参数,需要转码一下
    6. /// 当前举例:中文、链接
    7. String title = Uri.encodeComponent('张三');
    8. String url = Uri.encodeComponent('https://www.baidu.com');
    9. int age = 99;
    10. double price = 9.9;
    11. bool flag = true;
    12. /// 注意:使用 path拼接方式 传递 参数,会改变原来的 路由页面 Path
    13. /// 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
    14. /// 所以再次匹配pageA,找不到,需要还原一下,getOriginalPath(path)
    15. NavigatorUtil.push(context,'${Routers.pageA}?name=$name&title=$title&url=$url&age=$age&price=$price&flag=$flag');

    传参跳转 - 对象类型

    1. NavigatorUtil.push(
    2. context,
    3. Routers.pageB,
    4. arguments: TestParamsModel(
    5. name: 'jk',
    6. title: '张三',
    7. url: 'https://www.baidu.com',
    8. age: 99,
    9. price: 9.9,
    10. flag: true,
    11. )
    12. );

    拦截

    1. /// 监听路由栈状态
    2. class PageRouteObserver extends NavigatorObserver {
    3. ... ...
    4. @override
    5. void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
    6. super.didPush(route, previousRoute);
    7. /// 当前所在页面 Path
    8. String? currentRoutePath = getOriginalPath(previousRoute);
    9. /// 要前往的页面 Path
    10. String? newRoutePath = getOriginalPath(route);
    11. /// 拦截指定页面
    12. /// 如果从 PageA 页面,跳转到 PageD,将其拦截
    13. if(currentRoutePath == Routers.pageA) {
    14. if(newRoutePath == Routers.pageD) {
    15. assert((){
    16. debugPrint('准备从 PageA页面 进入 pageD页面,进行登陆信息验证');
    17. // if(验证不通过) {
    18. /// 注意:要延迟一帧
    19. WidgetsBinding.instance.addPostFrameCallback((_){
    20. // 我这里是pop,视觉上达到无法进入新页面的效果,
    21. // 正常业务是跳转到 登陆页面
    22. NavigatorUtil.back(navigatorKey.currentContext!);
    23. });
    24. // }
    25. return true;
    26. }());
    27. }
    28. }
    29. ... ...
    30. }
    31. ... ...
    32. }
    33. /// 获取原生路径
    34. /// 使用 path拼接方式 传递 参数,会改变原来的 路由页面 Path
    35. ///
    36. /// 比如:NavigatorUtil.push(context,'${Routers.pageA}?name=$name&title=$title&url=$url&age=$age&price=$price&flag=$flag');
    37. /// 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
    38. /// 所以再次匹配pageA,找不到,需要还原一下,getOriginalPath(path)
    39. String? getOriginalPath(Route<dynamic>? route) {
    40. // 获取原始的路由路径
    41. String? fullPath = route?.settings.name;
    42. if(fullPath != null) {
    43. // 使用正则表达式去除查询参数
    44. return fullPath.split('?')[0];
    45. }
    46. return fullPath;
    47. }

    演示效果

    全局通知

    有几种业务需求,需要在不重启应用的情况下,更新每个页面的数据

    比如 切换主题,什么暗夜模式,还有就是 切换登录 等等,这里我偷了个懒,没有走完整的业务,只是调用当前 已经存在的所有页面的 didChangeDependencies() 方法;

    注意核心代码 我写在 BaseStatefulPageState 里,所以只有 继承 BaseStatefulPage + BaseStatefulPageState页面才能被通知

    具体原理: InheritedWidget 的特性,Provider 就是基于它实现的
    从 Flutter 源码看 InheritedWidget 内部实现原理

    切换登录

    在每个页面的 didChangeDependencies 里处理逻辑,重新请求接口

    1. @override
    2. void didChangeDependencies() {
    3. var operate = GlobalOperateProvider.getGlobalOperate(context: context);
    4. assert((){
    5. debugPrint('HomeView.didChangeDependencies --- $operate');
    6. return true;
    7. }());
    8. // 切换用户
    9. // 正常业务流程是:从本地存储,拿到当前最新的用户ID,请求接口,我这里偷了个懒 😄
    10. // 直接使用随机数,模拟 不同用户ID
    11. if (operate == GlobalOperate.switchLogin) {
    12. runSwitchLogin = true;
    13. // 重新请求数据
    14. // 如果你想刷新的时候,显示loading,加上这个两行
    15. viewModel?.pageDataModel?.type = NotifierResultType.loading;
    16. viewModel?.pageDataModel?.refreshState();
    17. viewModel?.requestData(params: {'curPage': Random().nextInt(20)});
    18. }
    19. }

    这是两个基类的完整代码

    1. import 'package:flutter/material.dart';
    2. /// 在执行全局操作后,所有继承 BaseStatefulPageState 的子页面,
    3. /// 都会执行 didChangeDependencies() 方法,然后执行 build() 方法
    4. ///
    5. /// 具体原理:是 InheritedWidget 的特性
    6. /// https://loveky.github.io/2018/07/18/how-flutter-inheritedwidget-works/
    7. /// 全局操作类型
    8. enum GlobalOperate {
    9. /// 默认空闲
    10. idle,
    11. /// 切换登陆
    12. switchLogin,
    13. /// ... ...
    14. }
    15. /// 持有 全局操作状态 的 InheritedWidget
    16. class GlobalNotificationWidget extends InheritedWidget {
    17. GlobalNotificationWidget({
    18. required this.globalOperate,
    19. required super.child});
    20. final GlobalOperate globalOperate;
    21. static GlobalNotificationWidget? of(BuildContext context) {
    22. return context
    23. .dependOnInheritedWidgetOfExactType();
    24. }
    25. /// 通知所有建立依赖的 子Widget
    26. @override
    27. bool updateShouldNotify(covariant GlobalNotificationWidget oldWidget) =>
    28. oldWidget.globalOperate != globalOperate &&
    29. globalOperate != GlobalOperate.idle;
    30. }
    31. /// 具体使用的 全局操作 Widget
    32. ///
    33. /// 执行全局操作: GlobalOperateProvider.runGlobalOperate(context: context, operate: GlobalOperate.switchLogin);
    34. /// 获取全局操作类型 GlobalOperateProvider.getGlobalOperate(context: context)
    35. class GlobalOperateProvider extends StatefulWidget {
    36. const GlobalOperateProvider({super.key, required this.child});
    37. final Widget child;
    38. /// 执行全局操作
    39. static runGlobalOperate({
    40. required BuildContext? context,
    41. required GlobalOperate operate,
    42. }) {
    43. context
    44. ?.findAncestorStateOfType<_GlobalOperateProviderState>()
    45. ?._runGlobalOperate(operate: operate);
    46. }
    47. /// 获取全局操作类型
    48. static GlobalOperate? getGlobalOperate({required BuildContext? context}) {
    49. return context
    50. ?.findAncestorStateOfType<_GlobalOperateProviderState>()
    51. ?.globalOperate;
    52. }
    53. @override
    54. State createState() => _GlobalOperateProviderState();
    55. }
    56. class _GlobalOperateProviderState extends State<GlobalOperateProvider> {
    57. GlobalOperate globalOperate = GlobalOperate.idle;
    58. /// 执行全局操作
    59. _runGlobalOperate({required GlobalOperate operate}) {
    60. // 先重置
    61. globalOperate = GlobalOperate.idle;
    62. // 再赋值
    63. globalOperate = operate;
    64. /// 别忘了刷新,如果不刷新,子widget不会执行 didChangeDependencies 方法
    65. setState(() {});
    66. }
    67. @override
    68. Widget build(BuildContext context) {
    69. return GlobalNotificationWidget(
    70. globalOperate: globalOperate,
    71. child: widget.child,
    72. );
    73. }
    74. }

    演示效果

    最好执行完全局操作后,将全局操作状态,重置回 空闲,我是拦截器里面,这个在哪重置,大家随意

    1. /// Dio拦截器
    2. class DioInterceptor extends InterceptorsWrapper {
    3. @override
    4. void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    5. ... ...
    6. /// 重置 全局操作状态
    7. if (EnvConfig.isGlobalNotification) {
    8. GlobalOperateProvider.runGlobalOperate(
    9. context: navigatorKey.currentContext, operate: GlobalOperate.idle);
    10. }
    11. ... ...
    12. }
    13. }

    开发环境配置

    我直接创建了三个启动文件

    测试环境

    1. /// 开发环境 入口函数
    2. void main() => Application.runApplication(
    3. envTag: EnvTag.develop, // 开发环境
    4. platform: ApplicationPlatform.app, // 手机应用
    5. baseUrl: 'https://www.wanandroid.com/', // 域名
    6. proxyEnable: true, // 是否开启抓包
    7. caughtAddress: '192.168.1.3:8888', // 抓包工具的代理地址 + 端口
    8. isGlobalNotification: true, // 是否有全局通知操作,比如切换用户
    9. /// 异常抛出,会在终端会显示,可帮助开发阶段,快速定位异常所在,
    10. /// 但会阻断,后续代码执行,建议 非开发阶段 关闭
    11. throwError: false,
    12. );

    预发布环境

    1. /// 预发布环境 入口函数
    2. void main() => Application.runApplication(
    3. envTag: EnvTag.preRelease, // 预发布环境
    4. platform: ApplicationPlatform.app, // 手机应用
    5. baseUrl: 'https://www.wanandroid.com/', // 域名
    6. );

    正式环境

    1. /// 正式环境 入口函数
    2. void main() => Application.runApplication(
    3. envTag: EnvTag.release, // 正式环境
    4. platform: ApplicationPlatform.app, // 手机应用
    5. baseUrl: 'https://www.wanandroid.com/', // 域名
    6. );

    Application

    1. class Application {
    2. Application.runApplication({
    3. required EnvTag envTag, // 开发环境
    4. required String baseUrl, // 域名
    5. required ApplicationPlatform platform, // 平台
    6. bool proxyEnable = false, // 是否开启抓包
    7. String? caughtAddress, // 抓包工具的代理地址 + 端口
    8. bool isGlobalNotification = false, // 是否有全局通知操作,比如切换用户
    9. bool throwError = false // 异常抛出,会在终端会显示,可帮助开发阶段,快速定位异常所在,但会阻断,后续代码执行
    10. }) {
    11. EnvConfig.envTag = envTag;
    12. EnvConfig.baseUrl = baseUrl;
    13. EnvConfig.platform = platform;
    14. EnvConfig.proxyEnable = proxyEnable;
    15. EnvConfig.caughtAddress = caughtAddress;
    16. EnvConfig.isGlobalNotification = isGlobalNotification;
    17. EnvConfig.throwError = throwError;
    18. /// runZonedGuarded 全局异常监听,实现异常上报
    19. runZonedGuarded(() {
    20. /// 确保一些依赖,全部初始化
    21. WidgetsFlutterBinding.ensureInitialized();
    22. /// 监听全局Widget异常,如果发生,将该Widget替换掉
    23. ErrorWidget.builder = (FlutterErrorDetails flutterErrorDetails) {
    24. return Material(
    25. child: Center(
    26. child: Text("请联系客服。"),
    27. ),
    28. );
    29. };
    30. // 初始化路由
    31. Routers.configureRouters();
    32. // 运行App
    33. runApp(App());
    34. }, (Object error, StackTrace stack) {
    35. // 使用第三方服务(例如Sentry)上报错误
    36. // Sentry.captureException(error, stackTrace: stackTrace);
    37. });
    38. }
    39. }

    网络请求抓包

    在Dio里配置的;

    注意:如果开启了抓包,但没有启动 抓包工具,Dio 会报 连接异常 DioException [connection error]

    1. /// 代理抓包,测试阶段可能需要
    2. void proxy() {
    3. if (EnvConfig.proxyEnable) {
    4. if (EnvConfig.caughtAddress?.isNotEmpty ?? false) {
    5. (httpClientAdapter as IOHttpClientAdapter).createHttpClient = () {
    6. final client = HttpClient();
    7. client.findProxy = (uri) => 'PROXY ' + EnvConfig.caughtAddress!;
    8. client.badCertificateCallback = (cert, host, port) => true;
    9. return client;
    10. };
    11. }
    12. }
    13. }

    演示效果

    如何抓包

    https://juejin.cn/post/7131928652568231966

    https://juejin.cn/post/7035652365826916366

    核心基类

    Model基类

    1. class BaseModel<VM extends PageViewModel> {
    2. VM? vm;
    3. void onDispose() {
    4. vm = null;
    5. }
    6. }

    View基类

    1. abstract class BaseStatefulPage<VM extends PageViewModel> extends BaseViewModelStatefulWidget<VM> {
    2. BaseStatefulPage({super.key});
    3. @override
    4. BaseStatefulPageState createState();
    5. }
    6. abstract class BaseStatefulPageState<T extends BaseStatefulPage, VM extends PageViewModel>
    7. extends BaseViewModelStatefulWidgetState<T, VM>
    8. with AutomaticKeepAliveClientMixin {
    9. /// 定义对应的 viewModel
    10. VM? viewModel;
    11. /// 监听应用生命周期
    12. AppLifecycleListener? lifecycleListener;
    13. /// 获取应用状态
    14. AppLifecycleState? get lifecycleState =>
    15. SchedulerBinding.instance.lifecycleState;
    16. /// 是否打印 监听应用生命周期的 日志
    17. bool debugPrintLifecycleLog = false;
    18. /// 进行初始化ViewModel相关操作
    19. @override
    20. void initState() {
    21. super.initState();
    22. /// 初始化页面 属性、对象、绑定监听
    23. initAttribute();
    24. initObserver();
    25. /// 初始化ViewModel,并同步生命周期
    26. viewModel = viewBindingViewModel();
    27. /// 调用viewModel的生命周期,比如 初始化 请求网络数据 等
    28. viewModel?.onCreate();
    29. /// Flutter 低版本 使用 WidgetsBindingObserver,高版本 使用 AppLifecycleListener
    30. lifecycleListener = AppLifecycleListener(
    31. // 监听状态回调
    32. onStateChange: onStateChange,
    33. // 可见,并且可以响应用户操作时的回调
    34. onResume: onResume,
    35. // 可见,但无法响应用户操作时的回调
    36. onInactive: onInactive,
    37. // 隐藏时的回调
    38. onHide: onHide,
    39. // 显示时的回调
    40. onShow: onShow,
    41. // 暂停时的回调
    42. onPause: onPause,
    43. // 暂停后恢复时的回调
    44. onRestart: onRestart,
    45. // 当退出 并将所有视图与引擎分离时的回调(IOS 支持,Android 不支持)
    46. onDetach: onDetach,
    47. // 在退出程序时,发出询问的回调(IOS、Android 都不支持)
    48. onExitRequested: onExitRequested,
    49. );
    50. /// 页面布局完成后的回调函数
    51. lifecycleListener?.binding.addPostFrameCallback((_) {
    52. assert(context != null, 'addPostFrameCallback throw Error context');
    53. /// 初始化 需要context 的属性、对象、绑定监听
    54. initContextAttribute(context);
    55. initContextObserver(context);
    56. });
    57. }
    58. @override
    59. void didChangeDependencies() {
    60. assert((){
    61. debugPrint('BaseStatefulPage.didChangeDependencies --- ${GlobalOperateProvider.getGlobalOperate(context: context)}');
    62. return true;
    63. }());
    64. }
    65. /// 监听状态
    66. onStateChange(AppLifecycleState state) => mLog('app_state:$state');
    67. /// =============================== 根据应用状态的产生的各种回调 ===============================
    68. /// 可见,并且可以响应用户操作时的回调
    69. /// 比如从应用后台调度到前台时,在 onShow() 后面 执行
    70. onResume() => mLog('onResume');
    71. /// 可见,但无法响应用户操作时的回调
    72. onInactive() => mLog('onInactive');
    73. /// 隐藏时的回调
    74. onHide() => mLog('onHide');
    75. /// 显示时的回调,从应用后台调度到前台时
    76. onShow() => mLog('onShow');
    77. /// 暂停时的回调
    78. onPause() => mLog('onPause');
    79. /// 暂停后恢复时的回调
    80. onRestart() => mLog('onRestart');
    81. /// 这两个回调,不是所有平台都支持,
    82. /// 当退出 并将所有视图与引擎分离时的回调(IOS 支持,Android 不支持)
    83. onDetach() => mLog('onDetach');
    84. /// 在退出程序时,发出询问的回调(IOS、Android 都不支持)
    85. /// 响应 [AppExitResponse.exit] 将继续终止,响应 [AppExitResponse.cancel] 将取消终止。
    86. Future onExitRequested() async {
    87. mLog('onExitRequested');
    88. return AppExitResponse.exit;
    89. }
    90. /// BaseStatefulPageState的子类,重写 dispose()
    91. /// 一定要执行父类 dispose(),防止内存泄漏
    92. @override
    93. void dispose() {
    94. /// 销毁顺序
    95. /// 1、Model 销毁其持有的 ViewModel
    96. if(viewModel?.pageDataModel?.data is BaseModel?) {
    97. BaseModel? baseModel = viewModel?.pageDataModel?.data as BaseModel?;
    98. baseModel?.onDispose();
    99. }
    100. if(viewModel?.pageDataModel?.data is BasePagingModel?) {
    101. BasePagingModel? basePagingModel = viewModel?.pageDataModel?.data as
    102. BasePagingModel?;
    103. basePagingModel?.onDispose();
    104. }
    105. /// 2、ViewModel 销毁其持有的 View
    106. /// 3、ViewModel 销毁其持有的 Model
    107. viewModel?.onDispose();
    108. /// 4、View 销毁其持有的 ViewModel
    109. viewModel = null;
    110. /// 5、销毁监听App生命周期方法
    111. lifecycleListener?.dispose();
    112. super.dispose();
    113. }
    114. /// 是否保持页面状态
    115. @override
    116. bool get wantKeepAlive => false;
    117. /// View 持有对应的 ViewModel
    118. VM viewBindingViewModel();
    119. /// 子类重写,初始化 属性、对象
    120. /// 这里不是 网络请求操作,而是页面的初始化数据
    121. /// 网络请求操作,建议在viewModel.onCreate() 中实现
    122. void initAttribute();
    123. /// 子类重写,初始化 需要 context 的属性、对象
    124. void initContextAttribute(BuildContext context) {}
    125. /// 子类重写,初始化绑定监听
    126. void initObserver();
    127. /// 子类重写,初始化需要 context 的绑定监听
    128. void initContextObserver(BuildContext context) {}
    129. /// 输出日志
    130. void mLog(String info) {
    131. if (debugPrintLifecycleLog) {
    132. assert(() {
    133. debugPrint('--- $info');
    134. return true;
    135. }());
    136. }
    137. }
    138. /// 手机应用
    139. Widget appBuild(BuildContext context) => SizedBox();
    140. /// Web
    141. Widget webBuild(BuildContext context) => SizedBox();
    142. /// PC应用
    143. Widget pcBuild(BuildContext context) => SizedBox();
    144. @override
    145. Widget build(BuildContext context) {
    146. /// 使用 AutomaticKeepAliveClientMixin 需要 super.build(context);
    147. ///
    148. /// 注意:AutomaticKeepAliveClientMixin 只是保存页面状态,并不影响 build 方法执行
    149. /// 比如 PageVie的 子页面 使用了AutomaticKeepAliveClientMixin 保存状态,
    150. /// PageView切换子页面时,子页面的build的还是会执行
    151. if(wantKeepAlive) {
    152. super.build(context);
    153. }
    154. /// 和 GlobalNotificationWidget,建立依赖关系
    155. if(EnvConfig.isGlobalNotification) {
    156. GlobalNotificationWidget.of(context);
    157. }
    158. switch (EnvConfig.platform) {
    159. case ApplicationPlatform.app: {
    160. if (Platform.isAndroid || Platform.isIOS) {
    161. // 如果,还想根据当前设备屏幕尺寸细分,
    162. // 使用MediaQuery,拿到当前设备信息,进一步适配
    163. return appBuild(context);
    164. }
    165. }
    166. case ApplicationPlatform.web: {
    167. return webBuild(context);
    168. }
    169. case ApplicationPlatform.pc: {
    170. if(Platform.isWindows || Platform.isMacOS) {
    171. return pcBuild(context);
    172. }
    173. }
    174. }
    175. return Center(
    176. child: Text('当前平台未适配'),
    177. );
    178. }
    179. }

    ViewModel基类

    1. /// 基类
    2. abstract class BaseViewModel {
    3. }
    4. /// 页面继承的ViewModel,不直接使用 BaseViewModel,
    5. /// 是因为BaseViewModel基类里代码,还是不要太多为好,扩展创建新的子类就好
    6. abstract class PageViewModel<T extends State> extends BaseViewModel {
    7. /// 定义对应的 view
    8. BaseStatefulPageState? viewState;
    9. /// 使用 直接拿它
    10. T get state => viewState as T;
    11. /// 页面数据model
    12. PageDataModel? pageDataModel;
    13. /// 尽量在onCreate方法中编写初始化逻辑
    14. void onCreate();
    15. /// 对应的widget被销毁了,销毁相关引用对象,避免内存泄漏
    16. void onDispose() {
    17. viewState = null;
    18. pageDataModel = null;
    19. }
    20. /// 请求数据
    21. Future requestData({Map<String, dynamic>? params});
    22. }

    分页Model基类

    1. /// 内部 有分页列表集合 的实体需要继承 BasePagingModel
    2. class BasePagingModel<VM extends PageViewModel> {
    3. int? curPage;
    4. List? datas;
    5. int? offset;
    6. bool? over;
    7. int? pageCount;
    8. int? size;
    9. int? total;
    10. VM? vm;
    11. BasePagingModel({this.curPage, this.datas, this.offset, this.over,
    12. this.pageCount, this.size, this.total});
    13. void onDispose() {
    14. vm = null;
    15. }
    16. }
    17. /// 是分页列表 集合子项 实体需要继承 BasePagingItem
    18. class BasePagingItem {}

    分页处理核心类

    1. /// 分页数据相关
    2. /// 分页行为:下拉刷新/上拉加载更多
    3. enum PagingBehavior {
    4. /// 空闲,默认状态
    5. idle,
    6. /// 加载
    7. load,
    8. /// 刷新
    9. refresh;
    10. }
    11. /// 分页状态:执行完 下拉刷新/上拉加载更多后,得到的状态
    12. enum PagingState {
    13. /// 空闲,默认状态
    14. idle,
    15. /// 加载成功
    16. loadSuccess,
    17. /// 加载失败
    18. loadFail,
    19. /// 没有更多数据了
    20. loadNoData,
    21. /// 正在加载
    22. curLoading,
    23. /// 刷新成功
    24. refreshSuccess,
    25. /// 刷新失败
    26. refreshFail,
    27. /// 正在刷新
    28. curRefreshing,
    29. }
    30. /// 分页数据对象
    31. class PagingDataModel<DM extends BaseChangeNotifier, VM extends PageViewModel> {
    32. // 当前页码
    33. int curPage;
    34. // 总共多少页
    35. int pageCount;
    36. // 总共 数据数量
    37. int total;
    38. // 当前页 数据数量
    39. int size;
    40. /// 响应的完整数据
    41. /// 你可能还需要,响应数据的 其他字段,
    42. ///
    43. /// assert((){
    44. /// var mListModel = pageDataModel?.data as MessageListModel?; // 转化成对应的Model
    45. /// debugPrint('---pageCount:${mListModel?.pageCount}'); // 获取字段
    46. /// return true;
    47. /// }());
    48. dynamic data;
    49. // 分页参数 字段,一般情况都是固定的,以防万一
    50. String? curPageField;
    51. // 数据列表
    52. List<dynamic> listData = [];
    53. // 当前的PageDataModel
    54. DM? pageDataModel;
    55. // 当前的PageViewModel
    56. VM? pageViewModel;
    57. PagingBehavior pagingBehavior = PagingBehavior.idle;
    58. PagingState pagingState = PagingState.idle;
    59. PagingDataModel(
    60. {this.curPage = 0,
    61. this.pageCount = 0,
    62. this.total = 0,
    63. this.size = 0,
    64. this.data,
    65. this.curPageField = 'curPage',
    66. this.pageDataModel}) : listData = [];
    67. /// 这两个方法,由 RefreshLoadWidget 组件调用
    68. /// 加载更多,追加数据
    69. Future loadListData() async {
    70. PagingState pagingState = PagingState.curLoading;
    71. pagingBehavior = PagingBehavior.load;
    72. Map<String, dynamic>? param = {curPageField!: curPage++};
    73. PageViewModel? currentPageViewModel = await pageViewModel?.requestData(params: param);
    74. if(currentPageViewModel?.pageDataModel?.type == NotifierResultType.success) {
    75. // 没有更多数据了
    76. if(currentPageViewModel?.pageDataModel?.total == listData.length) {
    77. pagingState = PagingState.loadNoData;
    78. } else {
    79. pagingState = PagingState.loadSuccess;
    80. }
    81. } else {
    82. pagingState = PagingState.loadFail;
    83. }
    84. return pagingState;
    85. }
    86. /// 下拉刷新数据
    87. Future refreshListData() async {
    88. PagingState pagingState = PagingState.curRefreshing;
    89. pagingBehavior = PagingBehavior.refresh;
    90. curPage = 0;
    91. Map<String, dynamic>? param = {curPageField!: curPage};
    92. PageViewModel? currentPageViewModel = await pageViewModel?.requestData(params: param);
    93. if(currentPageViewModel?.pageDataModel?.type == NotifierResultType.success) {
    94. pagingState = PagingState.refreshSuccess;
    95. } else {
    96. pagingState = PagingState.refreshFail;
    97. }
    98. return pagingState;
    99. }
    100. }

    源码地址 

    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

  • 相关阅读:
    网络安全的学习方向和路线是怎么样的?
    MyBatis ognl.NoSuchPropertyException 或者 Invalid bound statement (not found)
    外汇天眼:亏亏亏,为什么亏损的总是我?大数据分析报告告诉你答案
    Microsoft Office Word 远程命令执行漏洞(CVE-2022-30190)分析与利用
    SpringCloudSleuth异步线程支持和传递
    Java异常处理
    [附源码]java毕业设计美妆销售系统
    2022杭电多校联赛第三场 题解
    JavaScript -- 01. 基础语法介绍
    47、Dynamic View Synthesis from Dynamic Monocular Video
  • 原文地址:https://blog.csdn.net/Lan_Se_Tian_Ma/article/details/139563305