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

演示效果

路由

配置

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.mzph.cn/pingmian/25438.shtml

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

相关文章

memory动态内存管理学习之unique_ptr

此头文件是动态内存管理库的一部分。std::unique_ptr 是一种智能指针&#xff0c;它通过指针持有并管理另一对象&#xff0c;并在 unique_ptr 离开作用域时释放该对象。在发生下列两者之一时&#xff0c;用关联的删除器释放对象&#xff1a; 管理它的 unique_ptr 对象被销毁。…

YOLOv8 极简分割代码并输出各类别像素占比

文章目录 前言功能概述必要环境一、代码结构1. 参数定义2. 定义检测器类3. 计算各类别像素占比3.1 遍历每个检测到的目标3.2 获取当前目标的掩码和类别3.3 将掩码转换为整数多边形3.4 创建空白掩码图像并填充多边形3.5 计算掩码像素数3.6 计算掩码多边形的质心3.7 计算像素占比…

发光二极管十大品牌

日常电路设计中&#xff0c;LED是必用的元器件之一&#xff0c;辅助判定电路异常。 十大发光二极管品牌-LED灯珠生产厂家哪家好-LED发光二极管厂家前十-Maigoo品牌榜

Zabbix6.0自定义监控项

文章目录 一、自定义监控整体流程二、自定义监控案例1、监控TCP 443端口案例2、监控服务器异地登入(带参监控项) 一、自定义监控整体流程 操作端流程备注Agent端1️⃣ linux&#xff1a;通过命令、脚本取出对应的值2️⃣ linux&#xff1a;根据zbx要求按照格式、编写配置文件、…

Sui Generis如何为艺术家弥合Web3的鸿沟

Sui Generis是一家于3月推出的NFT拍卖行&#xff0c;其联合创始人兼CEO Gab9说其愿景是——更好、更大、更强&#xff01; 表面上看&#xff0c;Sui Generis是备受欢迎的Tombheads NFT拍卖行的重新品牌化&#xff0c;该拍卖行今年早些时候从Fantom区块链迁移出来。但它于3月31…

找出链表倒数第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 …

如何学习Golang语言!

第一部分&#xff1a;Go语言概述 起源与设计哲学&#xff1a;Go语言由Robert Griesemer、Rob Pike和Ken Thompson三位Google工程师设计&#xff0c;旨在解决现代编程中的一些常见问题&#xff0c;如编译速度、运行效率和并发编程。主要特点&#xff1a;Go语言的语法简单、编译…

人体部位眼耳手腿分类数据集4376张4类别

数据集类型&#xff1a;图像分类用&#xff0c;不可用于目标检测无标注文件 数据集格式&#xff1a;仅仅包含jpg图片&#xff0c;每个类别文件夹下面存放着对应图片 图片数量(jpg文件个数)&#xff1a;4376 分类类别数&#xff1a;4 类别名称:["Ears","Eyes&quo…

计算机组成刷题一轮(包过版)

搭配食用 计算机组成原理一轮-CSDN博客 目录 一、计算机系统概述 选择 计算机系统组成 冯诺依曼机 软件和硬件的功能 CPU等概念 计算机系统的工作原理 机器字长 运行速度 求MIPS 编译程序 机器语言程序 平均CPI和CPU执行时间 综合应用 存储程序原理 二…

System Verilog实现流水灯

文章目录 一 System Verilog1.1 Systemverilog简介1.2 与verilog的区别1.2.1 两态数据类型&#xff08;1,0&#xff09;1.2.2 枚举类型和用户自定义类型1.2.3 数组与队列1.2.4 字符串1.2.5 结构体和联合体1.2.6 常量1.2.7 过程语句等等 二 流水灯代码三 实验效果总结参考资料 一…

基于睡眠声音评估睡眠质量

随着健康意识的增强&#xff0c;人们越来越关注睡眠质量。确保获得充足的高质量睡眠对于维持身体健康和心理平衡至关重要。专业的睡眠状态测量主要通过多导睡眠图&#xff08;PSG&#xff09;进行。然而&#xff0c;PSG会给受试者带来显著的身体负担&#xff0c;并且在没有专业…

十大人工智能企业

​​​​​​链接&#xff1a;​​​​​​人工智能品牌排行-人工智能十大公司-人工智能十大品牌-Maigoo品牌榜

C# WPF入门学习主线篇(二十一)—— 静态资源和动态资源

C# WPF入门学习主线篇&#xff08;二十一&#xff09;—— 静态资源和动态资源 欢迎来到C# WPF入门学习系列的第二十一篇。在上一章中&#xff0c;我们介绍了WPF中的资源和样式。本篇文章将深入探讨静态资源&#xff08;StaticResource&#xff09;和动态资源&#xff08;Dynam…

数据挖掘--认识数据

数据挖掘--引论 数据挖掘--认识数据 数据挖掘--数据预处理 数据挖掘--数据仓库与联机分析处理 数据挖掘--挖掘频繁模式、关联和相关性&#xff1a;基本概念和方法 数据挖掘--分类 数据挖掘--聚类分析&#xff1a;基本概念和方法 数据对象与属性类型 属性&#xff1a;是一…

# log.info(“消息发送成功“); 红色报错 解决方案

log.info(“消息发送成功”); 红色报错 解决方案 一、错误描述&#xff1a; 在使用 idea 创建 maven 项目导入 lombok 依赖时&#xff0c;出现 log.info 报红错误&#xff0c;检查导入依赖正确&#xff0c;网络正常&#xff0c;错误依旧。 二、解决方案&#xff1a; 1、在 i…

【Java毕业设计】基于JavaWeb的洗衣店管理系统

文章目录 摘要ABSTRACT目 录1 概述1.1 研究背景及意义1.2 国内外研究现状1.3 拟研究内容1.4 系统开发技术1.4.1 SpringBoot框架1.4.2 MySQL数据库1.4.3 MVC模式 2 系统需求分析2.1 可行性分析2.2 功能需求分析 3 系统设计3.1 功能模块设计3.2 系统流程设计3.3 数据库设计3.3.1 …

嵌入式中C语言经典的面试题分享

#error的作用是什么? #error 指令让预处理器发出一条错误信息,并且会中断编译过程。下面我们从Linux代码中抽取出来一小段代码并做修改得到示例代码: 这段示例代码很简单,当RX_BUF_IDX宏的值不为0~3时,在预处理阶段就会通过 #error 指令输出一条错误提示信息: "…

GPT-4与GPT-4O的区别详解:面向小白用户

1. 模型介绍 在人工智能的语言模型领域&#xff0c;OpenAI的GPT-4和GPT-4O是最新的成员。这两个模型虽然来源于相同的基础技术&#xff0c;但在功能和应用上有着明显的区别。 GPT-4&#xff1a;这是一个通用型语言模型&#xff0c;可以理解和生成自然语言。无论是写作、对话还…

【Python】探索 One-Class SVM:异常检测的利器

我已经从你的 全世界路过 像一颗流星 划过命运 的天空 很多话忍住了 不能说出口 珍藏在 我的心中 只留下一些回忆 &#x1f3b5; 牛奶咖啡《从你的全世界路过》 在数据科学和机器学习领域&#xff0c;异常检测&#xff08;Anomaly Detection&#xff09;是…

Mysql学习(七)——约束

文章目录 四、约束4.1 概述4.2 约束演示4.3 外键约束 总结 四、约束 4.1 概述 概念&#xff1a;约束是作用于表中字段上的规则&#xff0c;用于限制存储在表中的数据。目的&#xff1a;保证数据库中数据的正确、有效性和完整性。分类&#xff1a; 4.2 约束演示 根据需求&…