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

前言

当前案例 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

 修改前

修改后 

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 销毁其持有的 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();
  }

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

Model

class HomeListModel extends BaseModel {

    ... ... 

	ValueNotifier<int> tapNum = ValueNotifier<int>(0); // 点击次数

	@override
    void onDispose() {
	  tapNum.dispose();
      super.onDispose();
    }

    ... ...
	
}

... ...

View

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;

}

ViewModel

class HomeViewModel extends PageViewModel {

  HomeViewState? state;

  @override
  onCreate() {
    /// 转化成 对应View 状态类型
    state = viewState as HomeViewState;

    ... ... 

    /// 初始化 网络请求
    requestData();
  }

  @override
  onDispose() {
    ... ...

    /// 别忘了执行父类的 onDispose
    super.onDispose();
  }

  /// 请求数据
  @override
  Future<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;
      }

      return pageViewModel;
    } 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 {

    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;
    }

    return pageViewModel;
  }

  /// 登陆
  Future<PageViewModel> loginUser({
    required PageViewModel pageViewModel,
    Map<String, dynamic>? params,
    CancelToken? cancelToken,
  }) async {
    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;
    }

    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;
      }

      return pageViewModel;
    } 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> = 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<T> createState() => _NotifierPageWidgetState<T>();
}

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<T> 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<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}'),
    );
  }

  @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<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});

  @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<GlobalNotificationWidget>();
  }

  /// 通知所有建立依赖的 子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<GlobalOperateProvider> 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

核心基类

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});

  @override
  BaseStatefulPageState<BaseStatefulPage, VM> 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<AppExitResponse> onExitRequested() async {
    mLog('onExitRequested');
    return AppExitResponse.exit;
  }

  /// BaseStatefulPageState的子类,重写 dispose()
  /// 一定要执行父类 dispose(),防止内存泄漏
  @override
  void 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();
  }

  /// 是否保持页面状态
  @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('当前平台未适配'),
    );
  }

}

ViewModel基类

/// 基类
abstract class BaseViewModel {

}

/// 页面继承的ViewModel,不直接使用 BaseViewModel,
/// 是因为BaseViewModel基类里代码,还是不要太多为好,扩展创建新的子类就好
abstract class PageViewModel extends BaseViewModel {

  /// 定义对应的 view
  BaseStatefulPageState? 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 = [];

  // 当前的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<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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/694845.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于STM32开发的智能语音助理系统

⬇帮大家整理了单片机的资料 包括stm32的项目合集【源码开发文档】 点击下方蓝字即可领取&#xff0c;感谢支持&#xff01;⬇ 点击领取更多嵌入式详细资料 问题讨论&#xff0c;stm32的资料领取可以私信&#xff01; 目录 引言环境准备智能语音助理系统基础代码实现&#xff…

《python程序语言设计》2018版第5章第47题绘制随机球,在一个宽120高100的矩形里绘制随机的点

这个题其实并不难。 首先我们利用turtle功能绘制一个矩形&#xff0c;圆心点题里要求的是0&#xff0c;0 这个好办 然后我们根据宽120&#xff0c;高100计算一下。肯定是正负两个值参与其中。 坐标点如下 建立矩形代码如下 turtle.penup() turtle.goto(-60, 50) turtle.pend…

HQChart小程序教程4-动态控制手势滚动页面

动态控制手势滚动页面 示例效果canvas 控制页面滚动属性步骤1. 使用变量绑定disable-scroll2. 在手势处理函数中控制是否滚动页面 交流QQ群HQChart代码地址 示例效果 canvas 控制页面滚动属性 根据官方文档&#xff0c;disable-scroll 属性是控制画布手势是否可以滚动页面。 h…

解读下/etc/network/interfaces配置文件

/etc/network/interfaces 是一个常见的网络配置文件&#xff0c;通常在 Debian 及其衍生版本的 Linux 发行版中使用。该文件用于配置网络接口和网络连接参数&#xff0c;允许用户手动设置网络连接的属性&#xff0c;包括 IP 地址、子网掩码、网关、DNS 服务器等。 以下是一个可…

领导者在沟通中最容易犯的错误

本文讨论了领导者在沟通过程中如何避免成为传声筒&#xff0c;通过筛选、处理和总结信息&#xff0c;在向上、向下沟通时保持相关性和真实性&#xff0c;提高沟通效率和效果。原文: The Dumbest Mistake Leaders Make in Communication 中层管理者作为高层领导、下属团队和其他…

Effective Java 2 遇到多个构造器参数时要考虑使用构建器

第2个经验法则&#xff1a;用遇到多个构造器参数时要考虑使用构建器&#xff08;consider a builder when faced with many constructor parameters&#xff09; 上一条讨论了静态工厂相对于构造器来说有五大优势。但静态工厂和构造器有个共同的局限性:它 们都不能很好地扩展到…

【计算机毕业设计】283基于微信小程序校园订餐

&#x1f64a;作者简介&#xff1a;拥有多年开发工作经验&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。&#x1f339;赠送计算机毕业设计600个选题excel文件&#xff0c;帮助大学选题。赠送开题报告模板&#xff…

大水文之------端午练练JS好了

最近有点不太知道要干啥了&#xff0c;昨天看了集cocos的介绍&#xff0c;下载了个DashBoard&#xff0c;看了看里面的内容&#xff0c;确实有点小震惊&#xff0c;还有些免费的源码可以学习&#xff0c;挺好的。 昨天学习ts&#xff0c;感觉自己的js水平好像不太行&#xff0c…

【LeetCode】两数相加(基于单向链表)难度:中等

目录 理清题目 解题思路 题目代码 运行结果 我们来看一下题目描述&#xff1a; 理清题目 首先题目要求链表中的节点的值必须在[0,9]之间也就是说我们要处理的数字必为正整数&#xff0c;因此就不会涉及到太复杂的计算&#xff0c;题目其实就是要求对两个链表中的节点的值分…

【文件导出2】导出html文件数据

导出html文件数据 文章目录 导出html文件数据前言一、实现代码1.controller层2.接口层3.接口实现类4.FileUtil 工具类 二、文件导出效果总结 前言 springBoot项目实现在线导出html文件数据的功能。 一、实现代码 1.controller层 GetMapping("/record/_export") Ap…

目标检测数据集 - 垃圾桶满溢检测数据集下载「包含VOC、COCO、YOLO三种格式」

数据集介绍&#xff1a;垃圾桶满溢检测数据集&#xff0c;真实场景高质量图片数据&#xff0c;涉及场景丰富&#xff0c;比如城市道边垃圾桶满溢、小区垃圾桶满溢、社区垃圾桶满溢、农村道边垃圾桶满溢、垃圾集中处理点垃圾桶满溢、公园垃圾桶满溢数据等。数据集标注标签划分为…

RJ45 PCB布线

RJ45底盘接地和数字地通过一个1M欧姆的电阻和一个0.1uF的去耦电容隔离。其底盘接地和数字地的间距&#xff0c;必须比60mil宽。如图11及图12所示。 图11 典型变压器集成单RJ45的机箱/数字地平面 图12 典型RJ45和变压器分开的机箱/数字地平面https://www.bilibili.com/read/…

apache poi 插入“下一页分节符”并设置下一节纸张横向的一种方法

一、需求描述 我们知道&#xff0c;有时在word中需要同时存在不同的节&#xff0c;部分页面需要竖向、部分页面需要横向。本文就是用java调用apache poi来实现用代码生成上述效果。下图是本文实现的效果&#xff0c;供各位看官查阅&#xff0c;本文以一篇课文为例&#xff0c;…

Linux基础I/O

一&#xff0c;系统文件I/O 写文件: #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <string.h> int main() {umask(0);int fd open("myfile", O_WRO…

[imx6ull]Linux下的SocketCAN通信

文章目录 一、CAN总线协议1.简介2.电气属性3.通信原理①数据帧的帧格式&#xff1a;②总线同步③总线竞争④数据保护 二、Linux下CAN的操作1.硬件连接①CAN电平转换器②扩展板使用CAN 2.查询 can 信息3.开启/关闭 can4.发送/接收 can 数据5.设置 can 参数 三、CAN的回环测试四、…

力扣1712.将数组分成三个子数组的方案数

力扣1712.将数组分成三个子数组的方案数 确定左边界的值 然后二分求右边界的范围 右边界处的前缀和满足 2*s[i] < s[r] < (s[n] s[i]) / 2 int s[100010];const int N 1e97;class Solution {public:int waysToSplit(vector<int>& nums) {int n nums.siz…

springcloudalibaba项目注册nacos,在nacos上修改配置项不生效问题

一、背景 之前的项目启动正常,后来发现springcloudalibaba的各版本匹配不正确,于是对项目中的springboot、springcloud、springcloudalibaba版本进行匹配升级,nacos1.4.2匹配的springboot、springcloud、springcloudalibaba版本与我的项目中的版本比较接近,于是我便重新安…

1-5 C语言操作符

C语言提供了非常丰富的操作符&#xff0c;使得C语言使用起来非常的方便 算数操作符&#xff1a; 加 减 乘 除 取模 【 - * / %】 注&#xff1a;除号的两端都是整数的时候执行的是整数的除法&#xff0c;如果…

Macbook M芯片JDK的安装

Macbook M芯片JDK的安装 下载 搜索zulu&#xff1b; 进入这个网址 https://www.azul.com/downloads/#zulu 进入页面后向下滑动 选择对应选项 然后点击Download进行下载。 选择.dmg格式的安装包&#xff0c;点击。 安装 下载完成后&#xff0c;到下载目录&#xff0c;双击…

电感十大供应商

电感品牌-电感器品牌排行榜-电感十大品牌-Maigoo品牌榜