当前位置: 首页 > news >正文

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(),防止内存泄漏@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();}

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

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;}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> = 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),);},),... ... ),);}}

演示效果

路由

普通无参跳转

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

相关文章:

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

前言 当前案例 Flutter SDK版本&#xff1a;3.22.2 每当我们开始一个新项目&#xff0c;都会 引入常用库、封装工具类&#xff0c;配置环境等等&#xff0c;我参考了一些文档&#xff0c;将这些内容整合、简单修改、二次封装&#xff0c;得到了一个开箱即用的Flutter开发模版…...

私有仓库搭建

目前市面上比较常见的私有仓库搭建方法为&#xff1a; 通过 Sinopia 或 verdaccio 搭建&#xff08;Sinopia 已经停止维护&#xff0c;verdaccio 是 Fork 自 Sinopia&#xff0c;基本上大同小异&#xff09;&#xff0c;其优点是搭建简单&#xff0c;不需要其他服务。通过 cnp…...

axios设置 responseType为 “stream“流式获取后端数据

使用前景&#xff1a; 工作过程中遇到了后端接口响应过慢&#xff0c;前端界面一致loading的情况&#xff0c;这个时候可以尝试采用将Axios的responseType参数被设置为stream类型实现。 stream介绍&#xff1a; stream类型意味着你希望服务器响应的数据以Node.js流&#xff…...

Apache POI(使用Java读写Excel表格数据)

1.Apache POI简介 Apache POI是一个开源的Java库&#xff0c;用于操作Microsoft Office格式的文件。它支持各种Office文档的读写功能&#xff0c;包括Word文档、Excel电子表格、PowerPoint演示文稿、Outlook电子邮件等。Apache POI提供了一组API&#xff0c;使得Java开发者能够…...

golang中只用定义不用初始化的类型规律总结

在go语言的开发中&#xff0c;有很多的内置类型是我们只需要定义而不需要初始化的&#xff0c; 如上文中提到的bytes.Buffer&#xff0c; strings.Builder。 其实在go语言中官方给我们定义的很多的类型都只需要定义&#xff0c;不需要初始化。 他们都有2个共同的规律&#xff…...

数据库之PostgreSQL详解

一、PostgreSQL介绍 PostgreSQL是一个功能强大的 开源 的关系型数据库。底层基于C实现。 PostgreSQL的开源协议和Linux内核版本的开源协议是一样的。。BDS协议&#xff0c;这个协议基本和MIT开源协议一样&#xff0c;说人话&#xff0c;就是你可以对PostgreSQL进行一些封装&a…...

找出链表倒数第k个元素-链表题

LCR 140. 训练计划 II - 力扣&#xff08;LeetCode&#xff09; 快慢指针。快指针臂慢指针快cnt个元素到最后&#xff1b; class Solution { public:ListNode* trainingPlan(ListNode* head, int cnt) {struct ListNode* quick head;struct ListNode* slow head;for(int i …...

ssm629基于SSM的二手交易平台设计与开发+jsp【已测试】

前言&#xff1a;&#x1f469;‍&#x1f4bb; 计算机行业的同仁们&#xff0c;大家好&#xff01;作为专注于Java领域多年的开发者&#xff0c;我非常理解实践案例的重要性。以下是一些我认为有助于提升你们技能的资源&#xff1a; &#x1f469;‍&#x1f4bb; SpringBoot…...

【Unity】资源管理与热更 YooAsset+HybridCLR

1 前言 Unity资源管理与热更新该用什么方法&#xff1f;当然是YooAssetHybridCLR了&#xff0c;YooAsset负责资源管理与热更&#xff0c;HybridCLR负责支持代码热更。 但这里我就不自己讲了&#xff0c;我会提供相关学习链接&#xff08;前人栽树我躺平&#xff09;。 2 学习链…...

PDF批量加水印 与 去除水印实践

本文主要目标是尝试去除水印&#xff0c;但是为了准备测试数据&#xff0c;我们需要先准备好有水印的pdf测试文件。 注意&#xff1a;本文的去水印只针对文字悬浮图片悬浮两种特殊情况&#xff0c;即使是这两种情况也不代表一定都可以去除水印。 文章目录 批量添加透明图片水印…...

【MySQL】服务器配置和管理

本文使用的MySQL版本是8.0 MySQL服务器介绍 MySQL服务器通常说的是mysqld程序。 mysqld 是 MySQL 数据库服务器的核心程序&#xff0c;负责处理客户端的请求、管理数据库和执行数据库操作。管理员可以通过配置文件和各种工具来管理和监控 mysqld 服务器的运行 官方文档&…...

限流定义、算法、实施方案

限流定义 1、 时间 &#xff0c; 基于某段时间或某个时间点&#xff0c;即&#xff1a;时间窗口 2、资源&#xff1a; 对可用资源进行限制&#xff1a; QPS/连接数/传输速率/黑白名单等 分布式环境下&#xff0c;主流限流方案&#xff1a; 网关层限流&#xff1a;流量入口Ngi…...

[312. 戳气球] 动态规划寻找转移函数

Problem: 312. 戳气球 文章目录 思路Code 思路 这个哥们写的思路真的很牛逼&#xff0c;转载一下他。 戳气球题解 Code class Solution { public:int maxCoins(vector<int>& nums) {nums.insert(nums.begin(), 1);nums.push_back(1);int n nums.size();vector<v…...

以操作系统和Java的视角看“中断“

引言 fucking-java-concurrency主要解读了在开发过程中常常会遇到的Java并发问题&#xff0c;本文主要总结Java的中断原理和应用。 PS: https://github.com/WeiXiao-Hyy/blog整理了后端开发的知识网络&#xff0c;欢迎Star&#xff01; 操作系统的中断 什么是中断&#xff1…...

【运维】如何在Ubuntu 22上使用Python 3.8的虚拟环境

在Ubuntu 22上使用Python 3.8的虚拟环境安装Ryu是相对简单的。以下是一步一步的指南&#xff1a; https://qq742971636.blog.csdn.net/article/details/139566151 安装Python 3.8&#xff1a; 在Ubuntu 22上&#xff0c;Python 3.8可能不是默认安装的版本。你可以使用以下命令…...

门面模式Api网关(SpringCloudGateway)

1. 前言 当前通过Eureka、Nacos解决了服务注册和服务发现问题&#xff0c;使用Spring Cloud LoadBalance解决了负载均衡的需求&#xff0c;同时借助OpenFeign实现了远程调用。然而&#xff0c;现有的微服务接口都直接对外暴露&#xff0c;容易被外部访问。为保障对外服务的安全…...

玩转Matlab-Simscape(初级)- 09 - 在Simulink中创建曲柄滑块机构的控制模型

** 玩转Matlab-Simscape&#xff08;初级&#xff09;- 09 - 在Simulink中创建曲柄滑块机构的控制模型 ** 目录 玩转Matlab-Simscape&#xff08;初级&#xff09;- 09 - 在Simulink中创建曲柄滑块机构的控制模型 前言一、问题描述二、创建模型2.1 识别机构中的刚体2.2 确定刚…...

手撸一个java网关框架

手写一个简易的Java网关框架涉及到很多方面&#xff0c;但我会提供一个基本的框架概念和代码示例&#xff0c;帮助你理解网关的基本构建。以下是一个简单的Java网关框架的实现&#xff1a; 定义路由&#xff1a;需要一个路由表来映射请求的URL到对应的处理器。 请求处理&#x…...

亮数据代理IP助力高效数据采集

文章目录 &#x1f4d1;前言一、爬虫数据采集痛点二、代理IP解决爬虫痛点2.1 为什么可以2.2 本篇采用的代理IP 四、零代码获取数据4.1 前置背景4.2 亮数据浏览器自动抓取数据4.3 使用步骤&#xff1a; 五、数据集5.1 免费样本5.2 定制数据集 &#x1f324;️个人小结 &#x1f…...

VS2022,DLL1调用lib,lib调用DLL2

DLL1调用lib&#xff0c;lib调用DLL2 问题1&#xff1a;为什么在dll1中需要引入dll2的.lib文件 当你有一个工程&#xff08;dll1&#xff09;调用静态库&#xff08;lib&#xff09;&#xff0c;而静态库&#xff08;lib&#xff09;又调用另一个DLL&#xff08;dll2&#xf…...

Unity Mirror VR联机开发 房间篇

一、需求 在联机时通常有加入房间这个步骤&#xff0c;在mirror示例中也有相应的案例&#xff0c;但是那个比较复杂&#xff0c;我们做教育科普类不需要如此复杂&#xff0c;傻瓜式操作基本就可以了&#xff0c;所以我简化了步骤&#xff0c;省略了点击准备按钮这一步骤&#…...

二叉树—leetcode

前言 本篇博客我们来仔细说一下二叉树二叉树的一些OJ题目 请看完上一篇&#xff1a;数据结构-二叉树-CSDN博客 &#x1f493; 个人主页&#xff1a;普通young man-CSDN博客 ⏩ 文章专栏&#xff1a;LeetCode_普通young man的博客-CSDN博客 若有问题 评论区见&#x1f4dd; &…...

shell编程(二)——字符串与数组

本文为shell 编程的第二篇&#xff0c;介绍shell中的字符串和数组相关内容。 一、字符串 shell 字符串可以用单引号 ‘’&#xff0c;也可以用双引号 “”&#xff0c;也可以不用引号。 单引号的特点 单引号里不识别变量单引号里不能出现单独的单引号&#xff08;使用转义符…...

【数据结构】二叉树专题

前言 本篇博客我们来看一些二叉树的经典题型&#xff0c;也是对上篇博客的补充 &#x1f493; 个人主页&#xff1a;小张同学zkf ⏩ 文章专栏&#xff1a;数据结构 若有问题 评论区见&#x1f4dd; &#x1f389;欢迎大家点赞&#x1f44d;收藏⭐文章 ​ 目录 1.单值二叉树 …...

开源模型应用落地-LangChain高阶-LCEL-表达式语言(四)

一、前言 尽管现在的大语言模型已经非常强大,可以解决许多问题,但在处理复杂情况时,仍然需要进行多个步骤或整合不同的流程才能达到最终的目标。然而,现在可以利用langchain来使得模型的应用变得更加直接和简单。 LCEL是什么? LCEL是一种非常灵活和强大的语言,可以帮助您更…...

Python第二语言(九、Python第一阶段实操)

目录 1. json数据格式 2. Python与json之间的数据转换 3. pyecharts模块官网 4. pyecharts快速入门&#xff08;折线图&#xff09; 5. pyecharts全局配置选项 5.1 set_global_ops使用 5.1.1. title_opts 5.1.2 legend_opts 5.1.3 toolbox_opts 5.1.4 visualmap_opts…...

Java异常机制

1.异常概述和异常处理机制 异常(exception)概述 异常就是程序在运行时出现的意外的&#xff0c;不正常的情况。 若异常产生后没有正确的处理&#xff0c;会导致程序的中断&#xff0c;程序不继续执行,以致造成损失。 2.2 异常处理机制 所以我们在开发中要一套机制来处理各种可能…...

Aws EC2,kubeadm方式安装kubernetes(k8s)

版本 docker版本&#xff1a;20.10.25 k8s版本&#xff08;kubeadm&#xff0c;kubelet和kubectl&#xff09;&#xff1a;1.20.10-0 初始化 # 禁用 SELinux sudo setenforce 0 sudo sed -i s/^SELINUXenforcing$/SELINUXpermissive/ /etc/selinux/config# 关闭防火墙 sudo …...

python 比较 mysql 表结构差异

最近在做项目的时候&#xff0c;需要比对两个数据库的表结构差异&#xff0c;由于表数量比较多&#xff0c;人工比对的话需要大量时间&#xff0c;且不可复用&#xff0c;于是想到用 python 写一个脚本来达到诉求&#xff0c;下次有相同诉求的时候只需改 sql 文件名即可。 com…...

【RAG入门教程01】Langchian框架 v0.2介绍

LangChain 是一个开源框架&#xff0c;旨在简化使用大型语言模型 (LLM) 创建应用程序的过程。可以将其想象成一套使用高级语言工具进行搭建的乐高积木。 它对于想要构建复杂的基于语言的应用程序而又不必管理直接与语言模型交互的复杂性的开发人员特别有用。它简化了将这些模型…...

烘焙食品网站建设需求分析/百度推广优化技巧

5.11.1找出3~100以内所有的素数 ##找出3~100以内所有素数 #&#xff08;1&#xff09;考虑初始条件 ##n 3 #(2)循环的结束条件 ##n<100 #(3)重复需要干什么 ##判断n是否为素数 #&#xff08;4&#xff09;如何度过下一次循环 ##nn1 n 3 while n<100:i 2flag True # …...

重庆江北营销型网站建设公司推荐/谷歌浏览器搜索入口

统计表格是实验数据、统计结果或事物分类的一种有效表达形式&#xff0c;是科技论文中经常使用的一种特殊信息语言&#xff0c;是描述科技文献的重要工具和手段。在撰写科技论文的过程中&#xff0c;通过正确使用统计表格&#xff0c;对获取到的资料数据进行归纳、整理、统计学…...

网站建设需求原型/hao123影视

文章来源&#xff1a;Python小例子告别枯燥&#xff0c;通过学习有趣的小例子&#xff0c;扎实而系统的入门Python&#xff0c;从菜鸟到大师&#xff0c;个人觉得这是很靠谱的一种方法。通过一个又一个的小例子&#xff0c;真正领悟Python之强大&#xff0c;之简洁&#xff0c;…...

深圳罗湖网站建设公司/湛江百度网站快速排名

1&#xff09;count(1)与count(*)比较&#xff1a; 1、如果你的数据表没有主键&#xff0c;那么count(1)比count(*)快2、如果有主键的话&#xff0c;那主键&#xff08;联合主键&#xff09;作为count的条件也比count(*)要快3、如果你的表只有一个字段的话那count(*)就是最快的…...

开发网是什么意思/优化设计数学

轮询轮询方式是Nginx负载默认的方式&#xff0c;顾名思义&#xff0c;所有请求都按照时间顺序分配到不同的服务上&#xff0c;如果服务Down掉&#xff0c;可以自动剔除&#xff0c;如下配置后轮训10001服务和10002服务。upstream dalaoyang-server {server localhost:10001…...

wordpress无法跳转正确页面/游戏推广公司怎么接游戏的

项目介绍 一款 Java 语言基于 SpringBoot2.x、MybatisPlus、Vue、ElementUI、MySQL等框架精心打造的一款前后端分离框架&#xff0c;致力于实现模块化、组件化、可插拔的前后端分离架构敏捷开发框架&#xff0c;可用于快速搭建前后端分离后台管理系统&#xff0c;本着简化开发…...