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: falseprefer_const_constructors: falsepackage_names: null

 修改前

修改后 

MVVM

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

持有关系:

View持有 ViewModel;

Model持有ViewModel;

ViewModel持有View;

ViewModel持有Model;

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

/// BaseStatefulPageState的子类,重写 dispose()
/// 一定要执行父类 dispose(),防止内存泄漏
@override
void dispose() {/// 销毁顺序/// 1、Model 销毁其持有的 ViewModelif(viewModel?.pageDataModel?.data is BaseModel?) {BaseModel? baseModel = viewModel?.pageDataModel?.data as BaseModel?;baseModel?.onDispose();}if(viewModel?.pageDataModel?.data is BasePagingModel?) {BasePagingModel? basePagingModel = viewModel?.pageDataModel?.data as BasePagingModel?;basePagingModel?.onDispose();}/// 2、ViewModel 销毁其持有的 View/// 3、ViewModel 销毁其持有的 ModelviewModel?.onDispose();/// 4、View 销毁其持有的 ViewModelviewModel = null;/// 5、销毁监听App生命周期方法lifecycleListener?.dispose();super.dispose();
}

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

Model

class HomeListModel extends BaseModel {... ... ValueNotifier<int> tapNum = ValueNotifier<int>(0); // 点击次数@overridevoid onDispose() {tapNum.dispose();super.onDispose();}... ...}... ...

View

class HomeView extends BaseStatefulPage<HomeViewModel> {HomeView({super.key});@overrideHomeViewState createState() => HomeViewState();
}class HomeViewState extends BaseStatefulPageState<HomeView, HomeViewModel> {@overrideHomeViewModel viewBindingViewModel() {/// ViewModel 和 View 相互持有return HomeViewModel()..viewState = this;}/// 初始化 页面 属性@overridevoid initAttribute() {... ...}/// 初始化 页面 相关对象绑定@overridevoid initObserver() {... ...}@overridevoid dispose() {... ... /// BaseStatefulPageState的子类,重写 dispose()/// 一定要执行父类 dispose(),防止内存泄漏super.dispose();}ValueNotifier<int> tapNum = ValueNotifier<int>(0);@overrideWidget appBuild(BuildContext context) {... ...}/// 是否保存页面状态@overridebool get wantKeepAlive => true;}

ViewModel

class HomeViewModel extends PageViewModel {HomeViewState? state;@overrideonCreate() {/// 转化成 对应View 状态类型state = viewState as HomeViewState;... ... /// 初始化 网络请求requestData();}@overrideonDispose() {... .../// 别忘了执行父类的 onDisposesuper.onDispose();}/// 请求数据@overrideFuture<PageViewModel?> requestData({Map<String, dynamic>? params}) async {... ...}
}

网络请求

Get请求

class HomeRepository {/// 获取首页数据Future<PageViewModel> getHomeData({required PageViewModel pageViewModel,CancelToken? cancelToken,int curPage = 0,}) async {try {Response response = await DioClient().doGet('project/list/$curPage/json?cid=294', cancelToken: cancelToken);if(response.statusCode == REQUEST_SUCCESS) {/// 请求成功pageViewModel.pageDataModel?.type = NotifierResultType.success;/// ViewModel 和 Model 相互持有HomeListModel model = HomeListModel.fromJson(response.data);model.vm = pageViewModel;pageViewModel.pageDataModel?.data = model;} else {/// 请求成功,但业务不通过,比如没有权限pageViewModel.pageDataModel?.type = NotifierResultType.unauthorized;pageViewModel.pageDataModel?.errorMsg = response.statusMessage;}} on DioException catch (dioEx) {/// 请求异常pageViewModel.pageDataModel?.type = NotifierResultType.dioError;pageViewModel.pageDataModel?.errorMsg = dioErrorConversionText(dioEx);} catch (e) {/// 未知异常pageViewModel.pageDataModel?.type = NotifierResultType.fail;pageViewModel.pageDataModel?.errorMsg = (e as Map).toString();}return pageViewModel;}}

Post请求

class PersonalRepository {/// 注册Future<PageViewModel> registerUser({required PageViewModel pageViewModel,Map<String, dynamic>? params,CancelToken? cancelToken,}) async {try {Response response = await DioClient().doPost('user/register',params: params,cancelToken: cancelToken,);if(response.statusCode == REQUEST_SUCCESS) {/// 请求成功pageViewModel.pageDataModel?.type = NotifierResultType.success; // 请求成功/// ViewModel 和 Model 相互持有UserInfoModel model = UserInfoModel.fromJson(response.data)..isLogin = false;model.vm = pageViewModel;pageViewModel.pageDataModel?.data = model;} else {/// 请求成功,但业务不通过,比如没有权限pageViewModel.pageDataModel?.type = NotifierResultType.unauthorized;pageViewModel.pageDataModel?.errorMsg = response.statusMessage;}} on DioException catch (dioEx) {/// 请求异常pageViewModel.pageDataModel?.type = NotifierResultType.dioError;pageViewModel.pageDataModel?.errorMsg = dioErrorConversionText(dioEx);} catch (e) {/// 未知异常pageViewModel.pageDataModel?.type = NotifierResultType.fail;pageViewModel.pageDataModel?.errorMsg = (e as Map).toString();}return pageViewModel;}/// 登陆Future<PageViewModel> loginUser({required PageViewModel pageViewModel,Map<String, dynamic>? params,CancelToken? cancelToken,}) async {try {Response response = await DioClient().doPost('user/login',params: params,cancelToken: cancelToken,);if(response.statusCode == REQUEST_SUCCESS) {/// 请求成功pageViewModel.pageDataModel?.type = NotifierResultType.success;/// ViewModel 和 Model 相互持有UserInfoModel model = UserInfoModel.fromJson(response.data)..isLogin = true;model.vm = pageViewModel;pageViewModel.pageDataModel?.data = model;} else {/// 请求成功,但业务不通过,比如没有权限pageViewModel.pageDataModel?.type = NotifierResultType.unauthorized;pageViewModel.pageDataModel?.errorMsg = response.statusMessage;}} on DioException catch (dioEx) {/// 请求异常pageViewModel.pageDataModel?.type = NotifierResultType.dioError;pageViewModel.pageDataModel?.errorMsg = dioErrorConversionText(dioEx);} catch (e) {/// 未知异常pageViewModel.pageDataModel?.type = NotifierResultType.fail;pageViewModel.pageDataModel?.errorMsg = (e as Map).toString();}return pageViewModel;}}

分页数据请求

class MessageRepository {/// 分页列表Future<PageViewModel> getMessageData({required PageViewModel pageViewModel,CancelToken? cancelToken,int curPage = 0,}) async {try {Response response = await DioClient().doGet('article/list/$curPage/json', cancelToken: cancelToken);if(response.statusCode == REQUEST_SUCCESS) {/// 请求成功pageViewModel.pageDataModel?.type = NotifierResultType.success;/// 有分页pageViewModel.pageDataModel?.isPaging = true;/// 分页代码pageViewModel.pageDataModel?.correlationPaging(pageViewModel, MessageListModel.fromJson(response.data));} else {/// 请求成功,但业务不通过,比如没有权限pageViewModel.pageDataModel?.type = NotifierResultType.unauthorized;pageViewModel.pageDataModel?.errorMsg = response.statusMessage;}} on DioException catch (dioEx) {/// 请求异常pageViewModel.pageDataModel?.type = NotifierResultType.dioError;pageViewModel.pageDataModel?.errorMsg = dioErrorConversionText(dioEx);} catch (e) {/// 未知异常pageViewModel.pageDataModel?.type = NotifierResultType.fail;pageViewModel.pageDataModel?.errorMsg = (e as Map).toString();}return pageViewModel;}}

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

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

刷新页面

NotifierPageWidget

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

enum NotifierResultType {// 不检查notCheck,// 加载中loading,// 请求成功success,// 这种属于请求成功,但业务不通过,比如没有权限unauthorized,// 请求异常dioError,// 未知异常fail,
}typedef NotifierPageWidgetBuilder<T extends BaseChangeNotifier> = WidgetFunction(BuildContext context, PageDataModel model);/// 这个是配合 PageDataModel 类使用的
class NotifierPageWidget<T extends BaseChangeNotifier> extends StatefulWidget {NotifierPageWidget({super.key,required this.model,required this.builder,});/// 需要监听的数据观察类final PageDataModel? model;final NotifierPageWidgetBuilder builder;@override_NotifierPageWidgetState<T> createState() => _NotifierPageWidgetState<T>();
}class _NotifierPageWidgetState<T extends BaseChangeNotifier>extends State<NotifierPageWidget<T>> {PageDataModel? model;/// 刷新UIrefreshUI() => setState(() {model = widget.model;});/// 对数据进行绑定监听@overridevoid initState() {super.initState();model = widget.model;// 先清空一次已注册的Listener,防止重复触发model?.removeListener(refreshUI);// 添加监听model?.addListener(refreshUI);}@overridevoid didUpdateWidget(covariant NotifierPageWidget<T> oldWidget) {super.didUpdateWidget(oldWidget);if (oldWidget.model != widget.model) {// 先清空一次已注册的Listener,防止重复触发oldWidget.model?.removeListener(refreshUI);model = widget.model;// 添加监听model?.addListener(refreshUI);}}@overrideWidget build(BuildContext context) {if (model?.type == NotifierResultType.notCheck) {return widget.builder(context, model!);}if (model?.type == NotifierResultType.loading) {return Center(child: Text('加载中...'),);}if (model?.type == NotifierResultType.success) {if (model?.data == null) {return Center(child: Text('数据为空'),);}if(model?.isPaging ?? false) {var lists = model?.data?.datas as List<BasePagingItem>?;if(lists?.isEmpty ?? false){return Center(child: Text('列表数据为空'),);};}return widget.builder(context, model!);}if (model?.type == NotifierResultType.unauthorized) {return Center(child: Text('业务不通过:${model?.errorMsg}'),);}/// 异常抛出,会在终端会显示,可帮助开发阶段,快速定位异常所在,/// 但会阻断,后续代码执行,建议 非开发阶段 关闭if(EnvConfig.throwError) {throw Exception('${model?.errorMsg}');}if (model?.type == NotifierResultType.dioError) {return Center(child: Text('dioError异常:${model?.errorMsg}'),);}if (model?.type == NotifierResultType.fail) {return Center(child: Text('未知异常:${model?.errorMsg}'),);}return Center(child: Text('请联系客服:${model?.errorMsg}'),);}@overridevoid dispose() {widget.model?.removeListener(refreshUI);super.dispose();}
}

使用 

class HomeView extends BaseStatefulPage<HomeViewModel> {HomeView({super.key});@overrideHomeViewState createState() => HomeViewState();
}class HomeViewState extends BaseStatefulPageState<HomeView, HomeViewModel> { @overrideWidget appBuild(BuildContext context) {return Scaffold(... ... body: NotifierPageWidget<PageDataModel>(model: viewModel?.pageDataModel,builder: (context, dataModel) {final data = dataModel.data as HomeListModel?;... ... return Stack(children: [ListView.builder(padding: EdgeInsets.zero,itemCount: data?.datas?.length ?? 0,itemBuilder: (context, index) {return Container(width: MediaQuery.of(context).size.width,height: 50,alignment: Alignment.center,child: Text('${data?.datas?[index].title}'),);}),... ...],);}),);}}

ValueListenableBuilder

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

class HomeView extends BaseStatefulPage<HomeViewModel> {HomeView({super.key});@overrideHomeViewState createState() => HomeViewState();
}class HomeViewState extends BaseStatefulPageState<HomeView, HomeViewModel> {... ...  ValueNotifier<int> tapNum = ValueNotifier<int>(0);@overrideWidget appBuild(BuildContext context) {return Scaffold(appBar: AppBar(backgroundColor: AppBarTheme.of(context).backgroundColor,/// 局部刷新title: ValueListenableBuilder<int>(valueListenable: tapNum,builder: (context, value, _) {return Text('Home:$value',style: TextStyle(fontSize: 20),);},),... ... ),);}}

演示效果

路由

配置

class Routers {static FluroRouter router = FluroRouter();// 配置路由static void configureRouters() {router.notFoundHandler = Handler(handlerFunc: (_, __) {// 找不到路由时,返回指定提示页面return Scaffold(body: const Center(child: Text('404'),),);});// 初始化路由_initRouter();}// 设置页面// 页面标识static String root = '/';// 页面Astatic String pageA = '/pageA';// 页面Bstatic String pageB = '/pageB';// 页面Cstatic String pageC = '/pageC';// 页面Dstatic String pageD = '/pageD';// 注册路由static _initRouter() {// 根页面router.define(root,handler: Handler(handlerFunc: (_, __) => AppMainPage(),),);// 页面A 需要 非对象类型 参数router.define(pageA,handler: Handler(handlerFunc: (_, Map<String, List<String>> params) {// 获取路由参数String? name = params['name']?.first;String? title = params['title']?.first;String? url = params['url']?.first;String? age = params['age']?.first ?? '-1';String? price = params['price']?.first ?? '-1';String? flag = params['flag']?.first ?? 'false';return PageAView(name: name,title: title,url: url,age: int.parse(age),price: double.parse(price),flag: bool.parse(flag));},),);// 页面B 需要 对象类型 参数router.define(pageB,handler: Handler(handlerFunc: (context, Map<String, List<String>> params) {// 获取路由参数TestParamsModel? paramsModel = context?.settings?.arguments as TestParamsModel?;return PageBView(paramsModel: paramsModel);},),);// 页面C 无参数router.define(pageC,handler: Handler(handlerFunc: (_, __) => PageCView(),),);// 页面D 无参数router.define(pageD,handler: Handler(handlerFunc: (_, __) => PageDView(),),);}}

普通无参跳转

NavigatorUtil.push(context, Routers.pageA);

传参跳转 - 非对象类型

  /// 传递 非对象参数 方式/// 在path后面,使用 '?' 拼接,再使用 '&' 分割String name = 'jk';/// Invalid argument(s): Illegal percent encoding in URI/// 出现这个异常,说明相关参数,需要转码一下/// 当前举例:中文、链接String title = Uri.encodeComponent('张三');String url = Uri.encodeComponent('https://www.baidu.com');int age = 99;double price = 9.9;bool flag = true;/// 注意:使用 path拼接方式 传递 参数,会改变原来的 路由页面 Path/// path会变成:/pageA?name=jk&title=%E5%BC%A0%E4%B8%89&url=https%3A%2F%2Fwww.baidu.com&age=99&price=9.9&flag=true/// 所以在匹配pageA,找不到,需要还原一下,getOriginalPath(path)NavigatorUtil.push(context,'${Routers.pageA}?name=$name&title=$title&url=$url&age=$age&price=$price&flag=$flag');

传参跳转 - 对象类型

NavigatorUtil.push(context,Routers.pageB,arguments: TestParamsModel(name: 'jk',title: '张三',url: 'https://www.baidu.com',age: 99,price: 9.9,flag: true,)
);

拦截

/// 监听路由栈状态
class PageRouteObserver extends NavigatorObserver {... ...@overridevoid didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {super.didPush(route, previousRoute);/// 当前所在页面 PathString? currentRoutePath = getOriginalPath(previousRoute);/// 要前往的页面 PathString? newRoutePath = getOriginalPath(route);/// 拦截指定页面/// 如果从 PageA 页面,跳转到 PageD,将其拦截if(currentRoutePath == Routers.pageA) {if(newRoutePath == Routers.pageD) {assert((){debugPrint('准备从 PageA页面 进入 pageD页面,进行登陆信息验证');// if(验证不通过) {/// 注意:要延迟一帧WidgetsBinding.instance.addPostFrameCallback((_){// 我这里是pop,视觉上达到无法进入新页面的效果,// 正常业务是跳转到 登陆页面NavigatorUtil.back(navigatorKey.currentContext!);});// }return true;}());}}... ... }... ...}/// 获取原生路径
/// 使用 path拼接方式 传递 参数,会改变原来的 路由页面 Path
///
/// 比如:NavigatorUtil.push(context,'${Routers.pageA}?name=$name&title=$title&url=$url&age=$age&price=$price&flag=$flag');
/// path会变成:/pageA?name=jk&title=%E5%BC%A0%E4%B8%89&url=https%3A%2F%2Fwww.baidu.com&age=99&price=9.9&flag=true
/// 所以再次匹配pageA,找不到,需要还原一下,getOriginalPath(path)
String? getOriginalPath(Route<dynamic>? route) {// 获取原始的路由路径String? fullPath = route?.settings.name;if(fullPath != null) {// 使用正则表达式去除查询参数return fullPath.split('?')[0];}return fullPath;
}

演示效果

全局通知

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

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

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

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

切换登录

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

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

这是两个基类的完整代码

import 'package:flutter/material.dart';/// 在执行全局操作后,所有继承 BaseStatefulPageState 的子页面,
/// 都会执行 didChangeDependencies() 方法,然后执行 build() 方法
///
/// 具体原理:是 InheritedWidget 的特性
/// https://loveky.github.io/2018/07/18/how-flutter-inheritedwidget-works//// 全局操作类型
enum GlobalOperate {/// 默认空闲idle,/// 切换登陆switchLogin,/// ... ...
}/// 持有 全局操作状态 的 InheritedWidget
class GlobalNotificationWidget extends InheritedWidget {GlobalNotificationWidget({required this.globalOperate,required super.child});final GlobalOperate globalOperate;static GlobalNotificationWidget? of(BuildContext context) {return context.dependOnInheritedWidgetOfExactType<GlobalNotificationWidget>();}/// 通知所有建立依赖的 子Widget@overridebool updateShouldNotify(covariant GlobalNotificationWidget oldWidget) =>oldWidget.globalOperate != globalOperate &&globalOperate != GlobalOperate.idle;
}/// 具体使用的 全局操作 Widget
///
/// 执行全局操作: GlobalOperateProvider.runGlobalOperate(context: context, operate: GlobalOperate.switchLogin);
/// 获取全局操作类型 GlobalOperateProvider.getGlobalOperate(context: context)
class GlobalOperateProvider extends StatefulWidget {const GlobalOperateProvider({super.key, required this.child});final Widget child;/// 执行全局操作static runGlobalOperate({required BuildContext? context,required GlobalOperate operate,}) {context?.findAncestorStateOfType<_GlobalOperateProviderState>()?._runGlobalOperate(operate: operate);}/// 获取全局操作类型static GlobalOperate? getGlobalOperate({required BuildContext? context}) {return context?.findAncestorStateOfType<_GlobalOperateProviderState>()?.globalOperate;}@overrideState<GlobalOperateProvider> createState() => _GlobalOperateProviderState();
}class _GlobalOperateProviderState extends State<GlobalOperateProvider> {GlobalOperate globalOperate = GlobalOperate.idle;/// 执行全局操作_runGlobalOperate({required GlobalOperate operate}) {// 先重置globalOperate = GlobalOperate.idle;// 再赋值globalOperate = operate;/// 别忘了刷新,如果不刷新,子widget不会执行 didChangeDependencies 方法setState(() {});}@overrideWidget build(BuildContext context) {return GlobalNotificationWidget(globalOperate: globalOperate,child: widget.child,);}
}

演示效果

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

/// Dio拦截器
class DioInterceptor extends InterceptorsWrapper {@overridevoid onRequest(RequestOptions options, RequestInterceptorHandler handler) {... ... /// 重置 全局操作状态if (EnvConfig.isGlobalNotification) {GlobalOperateProvider.runGlobalOperate(context: navigatorKey.currentContext, operate: GlobalOperate.idle);}... ...}}

开发环境配置

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

测试环境

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

预发布环境

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

正式环境

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

Application

class Application {Application.runApplication({required EnvTag envTag, // 开发环境required String baseUrl, // 域名required ApplicationPlatform platform, // 平台bool proxyEnable = false, // 是否开启抓包String? caughtAddress, // 抓包工具的代理地址 + 端口bool isGlobalNotification = false, // 是否有全局通知操作,比如切换用户bool throwError = false // 异常抛出,会在终端会显示,可帮助开发阶段,快速定位异常所在,但会阻断,后续代码执行}) {EnvConfig.envTag = envTag;EnvConfig.baseUrl = baseUrl;EnvConfig.platform = platform;EnvConfig.proxyEnable = proxyEnable;EnvConfig.caughtAddress = caughtAddress;EnvConfig.isGlobalNotification = isGlobalNotification;EnvConfig.throwError = throwError;/// runZonedGuarded 全局异常监听,实现异常上报runZonedGuarded(() {/// 确保一些依赖,全部初始化WidgetsFlutterBinding.ensureInitialized();/// 监听全局Widget异常,如果发生,将该Widget替换掉ErrorWidget.builder = (FlutterErrorDetails flutterErrorDetails) {return Material(child: Center(child: Text("请联系客服。"),),);};// 初始化路由Routers.configureRouters();// 运行ApprunApp(App());}, (Object error, StackTrace stack) {// 使用第三方服务(例如Sentry)上报错误// Sentry.captureException(error, stackTrace: stackTrace);});}}

网络请求抓包

在Dio里配置的;

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

  /// 代理抓包,测试阶段可能需要void proxy() {if (EnvConfig.proxyEnable) {if (EnvConfig.caughtAddress?.isNotEmpty ?? false) {(httpClientAdapter as IOHttpClientAdapter).createHttpClient = () {final client = HttpClient();client.findProxy = (uri) => 'PROXY ' + EnvConfig.caughtAddress!;client.badCertificateCallback = (cert, host, port) => true;return client;};}}}

演示效果

如何抓包

https://juejin.cn/post/7131928652568231966

https://juejin.cn/post/7035652365826916366

核心基类

Model基类

class BaseModel<VM extends PageViewModel> {VM? vm;void onDispose() {vm = null;}
}

View基类

abstract class BaseStatefulPage<VM extends PageViewModel> extends BaseViewModelStatefulWidget<VM> {BaseStatefulPage({super.key});@overrideBaseStatefulPageState<BaseStatefulPage, VM> createState();
}abstract class BaseStatefulPageState<T extends BaseStatefulPage, VM extends PageViewModel>extends BaseViewModelStatefulWidgetState<T, VM>with AutomaticKeepAliveClientMixin {/// 定义对应的 viewModelVM? viewModel;/// 监听应用生命周期AppLifecycleListener? lifecycleListener;/// 获取应用状态AppLifecycleState? get lifecycleState =>SchedulerBinding.instance.lifecycleState;/// 是否打印 监听应用生命周期的 日志bool debugPrintLifecycleLog = false;/// 进行初始化ViewModel相关操作@overridevoid initState() {super.initState();/// 初始化页面 属性、对象、绑定监听initAttribute();initObserver();/// 初始化ViewModel,并同步生命周期viewModel = viewBindingViewModel();/// 调用viewModel的生命周期,比如 初始化 请求网络数据 等viewModel?.onCreate();/// Flutter 低版本 使用 WidgetsBindingObserver,高版本 使用 AppLifecycleListenerlifecycleListener = AppLifecycleListener(// 监听状态回调onStateChange: onStateChange,// 可见,并且可以响应用户操作时的回调onResume: onResume,// 可见,但无法响应用户操作时的回调onInactive: onInactive,// 隐藏时的回调onHide: onHide,// 显示时的回调onShow: onShow,// 暂停时的回调onPause: onPause,// 暂停后恢复时的回调onRestart: onRestart,// 当退出 并将所有视图与引擎分离时的回调(IOS 支持,Android 不支持)onDetach: onDetach,// 在退出程序时,发出询问的回调(IOS、Android 都不支持)onExitRequested: onExitRequested,);/// 页面布局完成后的回调函数lifecycleListener?.binding.addPostFrameCallback((_) {assert(context != null, 'addPostFrameCallback throw Error context');/// 初始化 需要context 的属性、对象、绑定监听initContextAttribute(context);initContextObserver(context);});}@overridevoid didChangeDependencies() {assert((){debugPrint('BaseStatefulPage.didChangeDependencies --- ${GlobalOperateProvider.getGlobalOperate(context: context)}');return true;}());}/// 监听状态onStateChange(AppLifecycleState state) => mLog('app_state:$state');/// =============================== 根据应用状态的产生的各种回调 ===============================/// 可见,并且可以响应用户操作时的回调/// 比如从应用后台调度到前台时,在 onShow() 后面 执行onResume() => mLog('onResume');/// 可见,但无法响应用户操作时的回调onInactive() => mLog('onInactive');/// 隐藏时的回调onHide() => mLog('onHide');/// 显示时的回调,从应用后台调度到前台时onShow() => mLog('onShow');/// 暂停时的回调onPause() => mLog('onPause');/// 暂停后恢复时的回调onRestart() => mLog('onRestart');/// 这两个回调,不是所有平台都支持,/// 当退出 并将所有视图与引擎分离时的回调(IOS 支持,Android 不支持)onDetach() => mLog('onDetach');/// 在退出程序时,发出询问的回调(IOS、Android 都不支持)/// 响应 [AppExitResponse.exit] 将继续终止,响应 [AppExitResponse.cancel] 将取消终止。Future<AppExitResponse> onExitRequested() async {mLog('onExitRequested');return AppExitResponse.exit;}/// BaseStatefulPageState的子类,重写 dispose()/// 一定要执行父类 dispose(),防止内存泄漏@overridevoid dispose() {/// 销毁顺序/// 1、Model 销毁其持有的 ViewModel/// 2、ViewModel 销毁其持有的 View/// 3、View 销毁其持有的 ViewModel/// 4、销毁监听App生命周期方法if(viewModel?.pageDataModel?.data is BaseModel?) {BaseModel? baseModel = viewModel?.pageDataModel?.data as BaseModel?;baseModel?.onDispose();}if(viewModel?.pageDataModel?.data is BasePagingModel?) {BasePagingModel? basePagingModel = viewModel?.pageDataModel?.data as BasePagingModel?;basePagingModel?.onDispose();}viewModel?.onDispose();viewModel = null;lifecycleListener?.dispose();super.dispose();}/// 是否保持页面状态@overridebool get wantKeepAlive => false;/// View 持有对应的 ViewModelVM viewBindingViewModel();/// 子类重写,初始化 属性、对象/// 这里不是 网络请求操作,而是页面的初始化数据/// 网络请求操作,建议在viewModel.onCreate() 中实现void initAttribute();/// 子类重写,初始化 需要 context 的属性、对象void initContextAttribute(BuildContext context) {}/// 子类重写,初始化绑定监听void initObserver();/// 子类重写,初始化需要 context 的绑定监听void initContextObserver(BuildContext context) {}/// 输出日志void mLog(String info) {if (debugPrintLifecycleLog) {assert(() {debugPrint('--- $info');return true;}());}}/// 手机应用Widget appBuild(BuildContext context) => SizedBox();/// WebWidget webBuild(BuildContext context) => SizedBox();/// PC应用Widget pcBuild(BuildContext context) => SizedBox();@overrideWidget build(BuildContext context) {/// 使用 AutomaticKeepAliveClientMixin 需要 super.build(context);////// 注意:AutomaticKeepAliveClientMixin 只是保存页面状态,并不影响 build 方法执行/// 比如 PageVie的 子页面 使用了AutomaticKeepAliveClientMixin 保存状态,/// PageView切换子页面时,子页面的build的还是会执行if(wantKeepAlive) {super.build(context);}/// 和 GlobalNotificationWidget,建立依赖关系if(EnvConfig.isGlobalNotification) {GlobalNotificationWidget.of(context);}switch (EnvConfig.platform) {case ApplicationPlatform.app: {if (Platform.isAndroid || Platform.isIOS) {// 如果,还想根据当前设备屏幕尺寸细分,// 使用MediaQuery,拿到当前设备信息,进一步适配return appBuild(context);}}case ApplicationPlatform.web: {return webBuild(context);}case ApplicationPlatform.pc: {if(Platform.isWindows || Platform.isMacOS) {return pcBuild(context);}}}return Center(child: Text('当前平台未适配'),);}}

ViewModel基类

/// 基类
abstract class BaseViewModel {}/// 页面继承的ViewModel,不直接使用 BaseViewModel,
/// 是因为BaseViewModel基类里代码,还是不要太多为好,扩展创建新的子类就好
abstract class PageViewModel extends BaseViewModel {/// 定义对应的 viewBaseStatefulPageState? viewState;PageDataModel? pageDataModel;/// 尽量在onCreate方法中编写初始化逻辑void onCreate();/// 对应的widget被销毁了,销毁相关引用对象,避免内存泄漏void onDispose() {viewState = null;pageDataModel = null;}/// 请求数据Future<PageViewModel?> requestData({Map<String, dynamic>? params});}

分页Model基类

/// 内部 有分页列表集合 的实体需要继承 BasePagingModel
class BasePagingModel<VM extends PageViewModel> {int? curPage;List<BasePagingItem>? datas;int? offset;bool? over;int? pageCount;int? size;int? total;VM? vm;BasePagingModel({this.curPage, this.datas, this.offset, this.over,this.pageCount, this.size, this.total});void onDispose() {vm = null;}
}/// 是分页列表 集合子项 实体需要继承 BasePagingItem
class BasePagingItem {}

分页处理核心类

/// 分页数据相关/// 分页行为:下拉刷新/上拉加载更多
enum PagingBehavior {/// 空闲,默认状态idle,/// 加载load,/// 刷新refresh;
}/// 分页状态:执行完 下拉刷新/上拉加载更多后,得到的状态
enum PagingState {/// 空闲,默认状态idle,/// 加载成功loadSuccess,/// 加载失败loadFail,/// 没有更多数据了loadNoData,/// 正在加载curLoading,/// 刷新成功refreshSuccess,/// 刷新失败refreshFail,/// 正在刷新curRefreshing,
}/// 分页数据对象
class PagingDataModel<DM extends BaseChangeNotifier, VM extends PageViewModel> {// 当前页码int curPage;// 总共多少页int pageCount;// 总共 数据数量int total;// 当前页 数据数量int size;// 完整的数据dynamic data;// 分页参数 字段,一般情况都是固定的,以防万一String? curPageField;// 数据列表List<dynamic> listData = [];// 当前的PageDataModelDM? pageDataModel;// 当前的PageViewModelVM? pageViewModel;PagingBehavior pagingBehavior = PagingBehavior.idle;PagingState pagingState = PagingState.idle;PagingDataModel({this.curPage = 0,this.pageCount = 0,this.total = 0,this.size = 0,this.data,this.curPageField = 'curPage',this.pageDataModel}) : listData = [];/// 这两个方法,由 RefreshLoadWidget 组件调用/// 加载更多,追加数据Future<PagingState> loadListData() async {PagingState pagingState = PagingState.curLoading;pagingBehavior = PagingBehavior.load;Map<String, dynamic>? param = {curPageField!: curPage++};PageViewModel? currentPageViewModel = await pageViewModel?.requestData(params: param);if(currentPageViewModel?.pageDataModel?.type == NotifierResultType.success) {// 没有更多数据了if(currentPageViewModel?.pageDataModel?.total == listData.length) {pagingState = PagingState.loadNoData;} else {pagingState = PagingState.loadSuccess;}} else {pagingState = PagingState.loadFail;}return pagingState;}/// 下拉刷新数据Future<PagingState> refreshListData() async {PagingState pagingState = PagingState.curRefreshing;pagingBehavior = PagingBehavior.refresh;curPage = 0;Map<String, dynamic>? param = {curPageField!: curPage};PageViewModel? currentPageViewModel = await pageViewModel?.requestData(params: param);if(currentPageViewModel?.pageDataModel?.type == NotifierResultType.success) {pagingState = PagingState.refreshSuccess;} else {pagingState = PagingState.refreshFail;}return pagingState;}}

源码地址 

GitHub - LanSeLianMa/flutter_develop_template: Flutter项目开发模版,开箱即用

参考文档

 Dio:https://juejin.cn/post/7360227158662807589

路由:Flutter中封装Fluro路由配置,以及无context跳转与传参 - 掘金

MVVM:https://juejin.cn/post/7166503123983269901

API

玩Android平台的开放 API;

玩Android 开放API-玩Android - wanandroid.com

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

喜讯!云起无垠入选《2024中国AI大模型产业图谱1.0版》

近日&#xff0c;数据猿与上海大数据联盟联合策划并启动了“2024全年度三大策划活动”&#xff0c;经过数月的精心筹备和严格筛选&#xff0c;通过直接申报交流、深入访谈调研、外部咨询评价以及匿名访谈等多维度交叉验证的方式&#xff0c;最终完成了《2024中国AI大模型产业图…

鸿蒙开发文件管理:【@ohos.securityLabel (数据标签)】

数据标签 该模块提供文件数据安全等级的相关功能&#xff1a;向应用程序提供查询、设置文件数据安全等级的JS接口。 说明&#xff1a; 本模块首批接口从API version 9开始支持。后续版本的新增接口&#xff0c;采用上角标单独标记接口的起始版本。 导入模块 import security…

【单片机毕业设计选题】-基于STM32和阿里云的家庭安全监测系统

系统功能: 此设计采用STM32单片机采集环境温湿度,烟雾浓度和一氧化碳浓度显示在OLED上&#xff0c;并将这些信息上报至阿里云平台。 1. 上电连接手机热点后自动连接阿里云&#xff0c;可通过阿里云平台收到系统上报的温湿度&#xff0c;烟雾 浓度&#xff0c;一氧化碳数据以…

大数据时代下哈尔滨等保测评的新挑战与对策

引言 大数据时代&#xff0c;信息爆炸式增长&#xff0c;数据成为了新时代的“石油”。作为黑龙江省的省会城市&#xff0c;哈尔滨在积极推进智慧城市建设的过程中&#xff0c;大数据技术的应用日益广泛&#xff0c;随之而来的是信息安全领域的新挑战&#xff0c;特别是对信息…

WEB基础--TOMCAT服务器

服务器概述 什么是服务器 服务器&#xff1a;就是一个提供为人民服务的机器&#xff0c;这里的服务器主要指计算机服务器&#xff0c;分为两种&#xff1a;服务器软件和硬件服务器&#xff1b; 服务器分类 1、硬件服务器&#xff1a;安装了服务器软件的主机。就相当于高配的…

AI绘画基础教学:我用AI做建筑设计,10分钟完成100个方案

人工智能进入大众视野&#xff0c;就是ChatGPT给所有人打开了一扇通往人工智能世界的大门&#xff0c;面对这样一个强大又不太好驾驭的工具&#xff0c;很多人都经历了从惊讶、到惊喜&#xff0c;再到不知道能干啥用的茫然。 AI能帮人们做什么&#xff1f;建筑行业有哪些专门针…

Linux网络 - json,网络计算服务器与客户端改进

文章目录 前言一、json1.引入库2. 使用步骤2.Calculator.hpp3.Task.hpp4.serverCal.hpp 新客户端 前言 本章内容主要对上一章的网络计算器客户端和服务器进行一些Bug修正与功能改进。 并学习如何使用json库和daemon函数。 一、json 在我们自己的电脑上一些软件的文件夹中&…

顶顶通呼叫中心中间件-限制最大通话时间(mod_cti基于FreeSWITCH)

顶顶通呼叫中心中间件-限制最大通话时间(mod_cti基于FreeSWITCH) 一、最大通话时间 1、配置拨号方案 1、点击拨号方案 ->2、在框中输入通话最大时长->3、点击添加->4、根据图中配置->5、勾选continue。修改拨号方案需要等待一分钟即可生效 action"sched…

《Brave New Words 》2.2 阅读理解的未来,让文字生动起来!

Part II: Giving Voice to the Social Sciences 第二部分&#xff1a;为社会科学发声 The Future of Reading Comprehension, Where Literature Comes Alive! 阅读理解的未来&#xff0c;让文字生动起来&#xff01; Saanvi, a ninth grader in India who attends Khan World S…

Echarts 在折线图平滑位置处添加该处信息

文章目录 需求分析需求 分析 通过自定义折线图的标签(label)来实现。在 ECharts 中,可以通过设置 series 中的 label.normal.formatter 属性来实现这一点。 需要注意的是拐点处symbol不能设置为 none,否则会展示不出 label ,以下是一个示例代码,演示了如何在折线图的相邻…

超详解——Python 元组详解——小白篇

目录 1. 元组简介 创建元组 2. 元组常用操作 访问元组元素 切片操作 合并和重复 成员操作符 内置函数 解包元组 元组方法 3. 默认集合类型 作为字典的键 作为函数参数 作为函数的返回值 存储多种类型的元素 4.元组的优缺点 优点 缺点 5.元组的使用场景 数据…

如何保证数据库和缓存的一致性

背景&#xff1a;为了提高查询效率&#xff0c;一般会用redis作为缓存。客户端查询数据时&#xff0c;如果能直接命中缓存&#xff0c;就不用再去查数据库&#xff0c;从而减轻数据库的压力&#xff0c;而且redis是基于内存的数据库&#xff0c;读取速度比数据库要快很多。 更新…

《web应用技术》第十一次作业

1、验证过滤器进行权限验证的原理。 代码展示&#xff1a; Slf4j WebFilter(urlPatterns "/*") public class LoginCheckFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) thro…

计算机网络 —— 数据链路层(无线局域网)

计算机网络 —— 数据链路层&#xff08;无线局域网&#xff09; 什么是无线局域网IEEE 802.11主要标准及其特点&#xff1a; 802.11的MAC帧样式 我们来看看无线局域网&#xff1a; 什么是无线局域网 无线局域网&#xff08;Wireless Local Area Network&#xff0c;简称WLAN…

平板消解加热台-温度均匀,防腐蚀-实验室化学分析

DBF系列防腐电热板 是精致路合金加热板块表面经进口高纯实验级PFATeflon氟塑料防腐不粘处理&#xff0c;专为实验室设计的电加热产品&#xff0c;是样品前处理中&#xff0c;加热、消解、煮沸、蒸酸、赶酸等处理的得力助手。可以满足物理、化学、生物、环保、制药、食品、饮品…

【个人博客搭建】(23)购买服务器、域名、备案

1、服务器主要是为了有一个公网的IP地址&#xff0c;方便我们可以通过网络随时访问 2、域名是对IP地址的一个替代。简单说IP地址可能不方便记忆&#xff0c;但是自己配置的域名会简单些&#xff0c;另外暴露IP地址也不安全。(虽然也能通过域名找到IP) 3、备案。这是政策。简单所…

PBox iOS端的应用隐藏、图片视频加密软件

哈喽&#xff0c;大家下午好&#xff01;相信大家的手机中一定存在很多的私密内容&#xff0c;比如软件、照片、视频或者文档文件&#xff0c;很多都是不方便让外人看到的&#xff0c;此时就需要一款隐藏工具&#xff0c;市面上这类软件大部分都是收费的&#xff0c;应大家的需…

DETR实现目标检测(一)-训练自己的数据集

1、DETR架构 DETR&#xff08;Detection Transformer&#xff09;是一种新型的目标检测模型&#xff0c;由Facebook AI Research (FAIR) 在2020年提出。DETR的核心思想是将目标检测任务视为一个直接的集合预测问题&#xff0c;而不是传统的两步或多步预测问题。这种方法的创新…

升级和维护老旧LabVIEW程序

在升级老旧LabVIEW程序至64位环境时&#xff0c;需要解决兼容性、性能和稳定性等问题。本文从软件升级、硬件兼容性、程序优化、故障修复等多个角度详细分析。具体包括64位迁移注意事项、修复页面跳转崩溃、解决关闭程序后残留进程的问题&#xff0c;确保程序在新环境中的平稳运…

RainBond 制作应用并上架【以ElasticSearch为例】

文章目录 安装 ElasticSearch 集群第 1 步:添加组件第 2 步:查看组件第 3 步:访问组件制作 ElasticSearch 组件准备工作ElasticSearch 集群原理尝试 Helm 安装 ES 集群RainBond 制作 ES 思路源代码Dockerfiledocker-entrypoint.shelasticsearch.yml制作组件第 1 步:添加组件…