当前案例 Flutter SDK版本:3.22.2
每当我们开始一个新项目,都会 引入常用库、封装工具类,配置环境等等,我参考了一些文档,将这些内容整合、简单修改、二次封装,得到了一个开箱即用的Flutter开发模版,即使看不懂封装的工具对象原理,也没关系,模版化的使用方式,小白也可以快速开发Flutter项目。
用到的依赖库
dio: ^5.4.3+1 // 网络请求
fluro: ^2.0.5 // 路由
pull_to_refresh: ^2.0.0 // 下拉刷新 / 上拉加载更多
sentry_flutter: ^7.8.0 // 异常上报
默认使用的是Flutter团队制定的规则,但每个开发团队规则都不一样,违反规则的地方会出现黄色波浪下划线,比如我定义常量喜欢字母全部大写,这和默认规则不符;
修改 Flutter项目里的 analysis_options.yaml
文件,找到 rules
,添加以下配置;
rules:
use_key_in_widget_constructors: false
prefer_const_constructors: false
package_names: null
修改前
修改后
- MVVM 设计模式,相信大家应该不陌生,这里并不是传统的MVVM,我魔改了,简单说一下每层主要负责做什么;
- Model: 数据相关操作;
- View:UI相关操作;
- ViewModel:业务逻辑相关操作。
持有关系:
View持有 ViewModel;
Model持有ViewModel;
ViewModel持有View;
ViewModel持有Model;
注意:这种持有关系,有很高的内存泄漏风险,所以我在基类的dispose()
中进行了销毁,子类重写一定要调用 super.dispose()
;
/// BaseStatefulPageState的子类,重写 dispose()
/// 一定要执行父类 dispose(),防止内存泄漏
@override
void dispose() {
/// 销毁顺序
/// 1、Model 销毁其持有的 ViewModel
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();
}
/// 2、ViewModel 销毁其持有的 View
/// 3、ViewModel 销毁其持有的 Model
viewModel?.onDispose();
/// 4、View 销毁其持有的 ViewModel
viewModel = null;
/// 5、销毁监听App生命周期方法
lifecycleListener?.dispose();
super.dispose();
}
基类放在文章最后说,这里先忽略;
class HomeListModel extends BaseModel {
... ...
ValueNotifier<int> tapNum = ValueNotifier<int>(0); // 点击次数
@override
void onDispose() {
tapNum.dispose();
super.onDispose();
}
... ...
}
... ...
class HomeView extends BaseStatefulPage<HomeViewModel> {
HomeView({super.key});
@override
HomeViewState createState() => HomeViewState();
}
class HomeViewState extends BaseStatefulPageState<HomeView, HomeViewModel> {
@override
HomeViewModel viewBindingViewModel() {
/// ViewModel 和 View 相互持有
return HomeViewModel()..viewState = this;
}
/// 初始化 页面 属性
@override
void initAttribute() {
... ...
}
/// 初始化 页面 相关对象绑定
@override
void initObserver() {
... ...
}
@override
void dispose() {
... ...
/// BaseStatefulPageState的子类,重写 dispose()
/// 一定要执行父类 dispose(),防止内存泄漏
super.dispose();
}
ValueNotifier<int> tapNum = ValueNotifier<int>(0);
@override
Widget appBuild(BuildContext context) {
... ...
}
/// 是否保存页面状态
@override
bool get wantKeepAlive => true;
}
class HomeViewModel extends PageViewModel<HomeViewState> {
@override
onCreate() {
/// 拿到 页面状态里的 对象、属性 等等
debugPrint('---executeSwitchLogin:${state.executeSwitchLogin}');
... ...
/// 初始化 网络请求
requestData();
}
@override
onDispose() {
... ...
/// 别忘了执行父类的 onDispose
super.onDispose();
}
/// 请求数据
@override
Future<PageViewModel?> requestData({Map<String, dynamic>? params}) async {
... ...
}
}
typedef JsonCoverEntity<T extends BaseModel> = T Function(Map<String, dynamic> json);
class BaseRepository {
/// 统一处理 响应数据,可以避免写 重复代码,但如果业务复杂,可能还是需要在原始写法上,扩展
/// 普通页面(非分页)数据请求 统一处理
Future<PageViewModel> httpPageRequest({
required PageViewModel pageViewModel,
required Future<Response> future,
required JsonCoverEntity jsonCoverEntity,
CancelToken? cancelToken,
int curPage = 1,
}) async {
try {
Response response = await future;
if (response.statusCode == REQUEST_SUCCESS) {
/// 请求成功
pageViewModel.pageDataModel?.type = NotifierResultType.success;
/// ViewModel 和 Model 相互持有
dynamic model = jsonCoverEntity(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.toString();
}
return pageViewModel;
}
/// 分页数据请求 统一处理
Future<PageViewModel> httpPagingRequest({
required PageViewModel pageViewModel,
required Future<Response> future,
required JsonCoverEntity jsonCoverEntity,
CancelToken? cancelToken,
int curPage = 1,
}) async {
try {
Response response = await future;
if (response.statusCode == REQUEST_SUCCESS) {
/// 请求成功
pageViewModel.pageDataModel?.type = NotifierResultType.success;
/// 有分页
pageViewModel.pageDataModel?.isPaging = true;
/// 分页代码
/// ViewModel 和 Model 相互持有代码,写着 correlationPaging() 里面
pageViewModel.pageDataModel?.correlationPaging(
pageViewModel,
jsonCoverEntity(response.data) as dynamic,
);
} 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.toString();
}
return pageViewModel;
}
}
class HomeRepository extends BaseRepository {
/// 获取首页数据
Future<PageViewModel> getHomeData({
required PageViewModel pageViewModel,
CancelToken? cancelToken,
int curPage = 1,
}) async =>
httpPageRequest(
pageViewModel: pageViewModel,
jsonCoverEntity: HomeListModel.fromJson,
future: DioClient().doGet('project/list/$curPage/json?cid=294', cancelToken: cancelToken),
cancelToken: cancelToken,
curPage: curPage);
/// 这是不使用 httpPageRequest 的原始写法,如果业务复杂,可能还是需要在原始写法上,扩展
// /// 获取首页数据
// Future<PageViewModel> getHomeData({
// required PageViewModel pageViewModel,
// CancelToken? cancelToken,
// int curPage = 1,
// }) 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.toString();
// }
//
// return pageViewModel;
// }
}
class PersonalRepository extends BaseRepository {
/// 注册
Future<PageViewModel> registerUser({
required PageViewModel pageViewModel,
Map<String, dynamic>? params,
CancelToken? cancelToken,
}) async =>
httpPageRequest(
pageViewModel: pageViewModel,
cancelToken: cancelToken,
jsonCoverEntity: UserInfoModel.fromJson,
future: DioClient().doPost(
'user/register',
params: params,
cancelToken: cancelToken,
));
/// 登陆
Future<PageViewModel> loginUser({
required PageViewModel pageViewModel,
Map<String, dynamic>? params,
CancelToken? cancelToken,
}) async =>
httpPageRequest(
pageViewModel: pageViewModel,
cancelToken: cancelToken,
jsonCoverEntity: UserInfoModel.fromJson,
future: DioClient().doPost(
'user/login',
params: params,
cancelToken: cancelToken,
));
/// 这是不使用 httpPageRequest 的原始写法,如果业务复杂,可能还是需要在原始写法上,扩展
// /// 注册
// 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);
// 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.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);
// 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.toString();
// }
//
// return pageViewModel;
// }
}
class MessageRepository extends BaseRepository {
/// 分页列表
Future<PageViewModel> getMessageData({
required PageViewModel pageViewModel,
CancelToken? cancelToken,
int curPage = 1,
}) async => httpPagingRequest(
pageViewModel: pageViewModel,
cancelToken: cancelToken,
jsonCoverEntity: MessageListModel.fromJson,
curPage: curPage,
future: DioClient().doGet('article/list/$curPage/json', cancelToken: cancelToken));
/// 这是不使用 httpPagingRequest 的原始写法,如果业务复杂,可能还是需要在原始写法上,扩展
// /// 分页列表
// Future<PageViewModel> getMessageData({
// required PageViewModel pageViewModel,
// CancelToken? cancelToken,
// int curPage = 1,
// }) 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;
//
// /// 分页代码
// /// ViewModel 和 Model 相互持有代码,写着 correlationPaging() 里面
// 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.toString();
// }
//
// return pageViewModel;
// }
}
剩下的 ResultFul API 风格请求,我就不一一演示了,DioClient 里都封装好了,照葫芦画瓢就好。
ResultFul API 风格
GET:从服务器获取一项或者多项数据
POST:在服务器新建一个资源
PUT:在服务器更新所有资源
PATCH:更新部分属性
DELETE:从服务器删除资源
这个组件是我封装的,和 ViewModel 里的 PageDataModel 绑定,当PageDataModel里的数据发生改变, 就可以通知 NotifierPageWidget 刷新;
enum NotifierResultType {
// 不检查
notCheck,
// 加载中
loading,
// 请求成功
success,
// 这种属于请求成功,但业务不通过,比如没有权限
unauthorized,
// 请求异常
dioError,
// 未知异常
fail,
}
typedef NotifierPageWidgetBuilder<T extends BaseChangeNotifier> = Widget
Function(BuildContext context, PageDataModel model);
/// 这个是配合 PageDataModel 类使用的
class NotifierPageWidget<T extends BaseChangeNotifier> extends StatefulWidget {
NotifierPageWidget({
super.key,
required this.model,
required this.builder,
});
/// 需要监听的数据观察类
final PageDataModel? model;
final NotifierPageWidgetBuilder builder;
@override
_NotifierPageWidgetState<T> createState() => _NotifierPageWidgetState<T>();
}
class _NotifierPageWidgetState<T extends BaseChangeNotifier>
extends State<NotifierPageWidget<T>> {
PageDataModel? model;
/// 刷新UI
refreshUI() => setState(() {
model = widget.model;
});
/// 对数据进行绑定监听
@override
void initState() {
super.initState();
model = widget.model;
// 先清空一次已注册的Listener,防止重复触发
model?.removeListener(refreshUI);
// 添加监听
model?.addListener(refreshUI);
}
@override
void didUpdateWidget(covariant NotifierPageWidget<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.model != widget.model) {
// 先清空一次已注册的Listener,防止重复触发
oldWidget.model?.removeListener(refreshUI);
model = widget.model;
// 添加监听
model?.addListener(refreshUI);
}
}
@override
Widget build(BuildContext context) {
if (model?.type == NotifierResultType.notCheck) {
return widget.builder(context, model!);
}
if (model?.type == NotifierResultType.loading) {
return Center(
child: Text('加载中...'),
);
}
if (model?.type == NotifierResultType.success) {
if (model?.data == null) {
return Center(
child: Text('数据为空'),
);
}
if(model?.isPaging ?? false) {
var lists = model?.data?.datas as List<BasePagingItem>?;
if(lists?.isEmpty ?? false){
return Center(
child: Text('列表数据为空'),
);
};
}
return widget.builder(context, model!);
}
if (model?.type == NotifierResultType.unauthorized) {
return Center(
child: Text('业务不通过:${model?.errorMsg}'),
);
}
/// 异常抛出,会在终端会显示,可帮助开发阶段,快速定位异常所在,
/// 但会阻断,后续代码执行,建议 非开发阶段 关闭
if(EnvConfig.throwError) {
throw Exception('${model?.errorMsg}');
}
if (model?.type == NotifierResultType.dioError) {
return Center(
child: Text('dioError异常:${model?.errorMsg}'),
);
}
if (model?.type == NotifierResultType.fail) {
return Center(
child: Text('未知异常:${model?.errorMsg}'),
);
}
return Center(
child: Text('请联系客服:${model?.errorMsg}'),
);
}
@override
void dispose() {
widget.model?.removeListener(refreshUI);
super.dispose();
}
}
使用
class HomeView extends BaseStatefulPage<HomeViewModel> {
HomeView({super.key});
@override
HomeViewState createState() => HomeViewState();
}
class HomeViewState extends BaseStatefulPageState<HomeView, HomeViewModel> {
@override
Widget appBuild(BuildContext context) {
return Scaffold(
... ...
body: NotifierPageWidget<PageDataModel>(
model: viewModel?.pageDataModel,
builder: (context, dataModel) {
final data = dataModel.data as HomeListModel?;
... ...
return Stack(
children: [
ListView.builder(
padding: EdgeInsets.zero,
itemCount: data?.datas?.length ?? 0,
itemBuilder: (context, index) {
return Container(
width: MediaQuery.of(context).size.width,
height: 50,
alignment: Alignment.center,
child: Text('${data?.datas?[index].title}'),
);
}),
... ...
],
);
}
),
);
}
}
这个就是Flutter自带的组件,配合ValueNotifier使用,我主要用它做局部刷新;
class HomeView extends BaseStatefulPage<HomeViewModel> {
HomeView({super.key});
@override
HomeViewState createState() => HomeViewState();
}
class HomeViewState extends BaseStatefulPageState<HomeView, HomeViewModel> {
... ...
ValueNotifier<int> tapNum = ValueNotifier<int>(0);
@override
Widget appBuild(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: AppBarTheme.of(context).backgroundColor,
/// 局部刷新
title: ValueListenableBuilder<int>(
valueListenable: tapNum,
builder: (context, value, _) {
return Text(
'Home:$value',
style: TextStyle(
fontSize: 20,
),
);
},
),
... ...
),
);
}
}
class Routers {
static FluroRouter router = FluroRouter();
// 配置路由
static void configureRouters() {
router.notFoundHandler = Handler(handlerFunc: (_, __) {
// 找不到路由时,返回指定提示页面
return Scaffold(
body: const Center(
child: Text('404'),
),
);
});
// 初始化路由
_initRouter();
}
// 设置页面
// 页面标识
static String root = '/';
// 页面A
static String pageA = '/pageA';
// 页面B
static String pageB = '/pageB';
// 页面C
static String pageC = '/pageC';
// 页面D
static String pageD = '/pageD';
// 注册路由
static _initRouter() {
// 根页面
router.define(
root,
handler: Handler(
handlerFunc: (_, __) => AppMainPage(),
),
);
// 页面A 需要 非对象类型 参数
router.define(
pageA,
handler: Handler(
handlerFunc: (_, Map<String, List<String>> params) {
// 获取路由参数
String? name = params['name']?.first;
String? title = params['title']?.first;
String? url = params['url']?.first;
String? age = params['age']?.first ?? '-1';
String? price = params['price']?.first ?? '-1';
String? flag = params['flag']?.first ?? 'false';
return PageAView(
name: name,
title: title,
url: url,
age: int.parse(age),
price: double.parse(price),
flag: bool.parse(flag)
);
},
),
);
// 页面B 需要 对象类型 参数
router.define(
pageB,
handler: Handler(
handlerFunc: (context, Map<String, List<String>> params) {
// 获取路由参数
TestParamsModel? paramsModel = context?.settings?.arguments as TestParamsModel?;
return PageBView(paramsModel: paramsModel);
},
),
);
// 页面C 无参数
router.define(
pageC,
handler: Handler(
handlerFunc: (_, __) => PageCView(),
),
);
// 页面D 无参数
router.define(
pageD,
handler: Handler(
handlerFunc: (_, __) => PageDView(),
),
);
}
}
NavigatorUtil.push(context, Routers.pageA);
ElevatedButton(
onPressed: () {
/// 传递 非对象参数 方式一:通过 拼接 传参数 了解即可,推荐使用方式二
/// 在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')
.then((result) {
assert(() {
debugPrint('PageA Pop的返回值:$result');
return true;
}());
});
},
child: Text(StrOrder.noObjectToPageA),
),
ElevatedButton(
onPressed: () {
/// 传递 非对象参数 方式二:通过 arguments 传参数,推荐
String name = 'jk';
String title = '张三';
String url = 'https://www.baidu.com';
int age = 99;
double price = 9.9;
bool flag = true;
NavigatorUtil.push(context,
Routers.pageA2,
arguments: {
'name': name,
'title': title,
'url': url,
'age': age,
'price': price,
'flag': flag,
}
).then((result){
assert(() {
debugPrint('PageA2 Pop的返回值:$result');
return true;
}());
});
},
child: Text(StrOrder.noObjectToPageA2),
),
NavigatorUtil.push(
context,
Routers.pageB,
arguments: TestParamsModel(
name: 'jk',
title: '张三',
url: 'https://www.baidu.com',
age: 99,
price: 9.9,
flag: true,
)
);
/// 监听路由栈状态
class PageRouteObserver extends NavigatorObserver {
... ...
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
super.didPush(route, previousRoute);
/// 当前所在页面 Path
String? currentRoutePath = getOriginalPath(previousRoute);
/// 要前往的页面 Path
String? newRoutePath = getOriginalPath(route);
/// 拦截指定页面
/// 如果从 PageA 页面,跳转到 PageD,将其拦截
if(currentRoutePath == Routers.pageA) {
if(newRoutePath == Routers.pageD) {
assert((){
debugPrint('准备从 PageA页面 进入 pageD页面,进行登陆信息验证');
return true;
}());
// if(验证不通过) {
/// 注意:要延迟一帧
WidgetsBinding.instance.addPostFrameCallback((_){
// 我这里是pop,视觉上达到无法进入新页面的效果,
// 正常业务是跳转到 登陆页面
NavigatorUtil.back(navigatorKey.currentContext!);
});
// }
}
}
... ...
}
... ...
}
/// 获取原生路径
/// 使用 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 就是基于它实现的;
https://loveky.github.io/2018/07/18/how-flutter-inheritedwidget-works/
在每个页面的 didChangeDependencies 里处理逻辑,重新请求接口;
@override
void didChangeDependencies() {
var operate = GlobalOperateProvider.getGlobalOperate(context: context);
assert((){
debugPrint('HomeView.didChangeDependencies --- $operate');
return true;
}());
// 切换用户
// 正常业务流程是:从本地存储,拿到当前最新的用户ID,请求接口,我这里偷了个懒 😄
// 直接使用随机数,模拟 不同用户ID
if (operate == GlobalOperate.switchLogin) {
executeSwitchLogin = true;
// 重新请求数据
// 如果你想刷新的时候,显示loading,加上这两行
viewModel?.pageDataModel?.type = NotifierResultType.loading;
viewModel?.pageDataModel?.refreshState();
viewModel?.requestData(params: {'curPage': Random().nextInt(20)});
}
}
这是两个基类的完整代码
import 'package:flutter/material.dart';
/// 在执行全局操作后,所有继承 BaseStatefulPageState 的子页面,
/// 都会执行 didChangeDependencies() 方法,然后执行 build() 方法
///
/// 具体原理:是 InheritedWidget 的特性
/// https://loveky.github.io/2018/07/18/how-flutter-inheritedwidget-works/
/// 全局操作类型
enum GlobalOperate {
/// 默认空闲
idle,
/// 切换登陆
switchLogin,
/// ... ...
}
/// 持有 全局操作状态 的 InheritedWidget
class GlobalNotificationWidget extends InheritedWidget {
GlobalNotificationWidget({
required this.globalOperate,
required super.child});
final GlobalOperate globalOperate;
static GlobalNotificationWidget? of(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<GlobalNotificationWidget>();
}
/// 通知所有建立依赖的 子Widget
@override
bool updateShouldNotify(covariant GlobalNotificationWidget oldWidget) =>
oldWidget.globalOperate != globalOperate &&
globalOperate != GlobalOperate.idle;
}
/// 具体使用的 全局操作 Widget
///
/// 执行全局操作: GlobalOperateProvider.runGlobalOperate(context: context, operate: GlobalOperate.switchLogin);
/// 获取全局操作类型 GlobalOperateProvider.getGlobalOperate(context: context)
class GlobalOperateProvider extends StatefulWidget {
const GlobalOperateProvider({super.key, required this.child});
final Widget child;
/// 执行全局操作
static runGlobalOperate({
required BuildContext? context,
required GlobalOperate operate,
}) {
context
?.findAncestorStateOfType<_GlobalOperateProviderState>()
?._runGlobalOperate(operate: operate);
}
/// 获取全局操作类型
static GlobalOperate? getGlobalOperate({required BuildContext? context}) {
return context
?.findAncestorStateOfType<_GlobalOperateProviderState>()
?.globalOperate;
}
@override
State<GlobalOperateProvider> createState() => _GlobalOperateProviderState();
}
class _GlobalOperateProviderState extends State<GlobalOperateProvider> {
GlobalOperate globalOperate = GlobalOperate.idle;
/// 执行全局操作
_runGlobalOperate({required GlobalOperate operate}) {
// 先重置
globalOperate = GlobalOperate.idle;
// 再赋值
globalOperate = operate;
/// 别忘了刷新,如果不刷新,子widget不会执行 didChangeDependencies 方法
setState(() {});
}
@override
Widget build(BuildContext context) {
return GlobalNotificationWidget(
globalOperate: globalOperate,
child: widget.child,
);
}
}
最好执行完全局操作后,将全局操作状态,重置回 空闲,我是拦截器里面,这个在哪重置,大家随意;
/// Dio拦截器
class DioInterceptor extends InterceptorsWrapper {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
... ...
/// 重置 全局操作状态
if (EnvConfig.isGlobalNotification) {
GlobalOperateProvider.runGlobalOperate(
context: navigatorKey.currentContext, operate: GlobalOperate.idle);
}
... ...
}
}
我直接创建了三个启动文件;
/// 开发环境 入口函数
void main() => Application.runApplication(
envTag: EnvTag.dev, // 开发环境
platform: ApplicationPlatform.app, // 手机应用
baseUrl: 'https://www.wanandroid.com/', // 域名
proxyEnable: false, // 是否开启抓包
caughtAddress: '192.168.1.3:8888', // 抓包工具的代理地址 + 端口
isGlobalNotification: true, // 是否有全局通知操作,比如切换用户
/// 异常抛出,会在终端会显示,可帮助开发阶段,快速定位异常所在,
/// 但会阻断,后续代码执行,建议 非开发阶段 关闭
throwError: true,
pushErrToSentry: false, // 是否开启 异常上报到 Sentry
sentryDNS: 'https://123456789191111@xxx.com/2' // Sentry DNS 标识
/// AndroidManifest.xml 中
/// 开启网络权限
/// <uses-permission android:name="android.permission.INTERNET" />
///
/// 如果使用 http,还需要配置
/// <application
/// android:usesCleartextTraffic="true"
///
// 'http://123456789191111@IP:9000/2';
/// 配置完 域名 和 SSL证书后,http 改为 https,IP 替换为 域名,
// 'https://123456789191111@域名/2';
);
/// 预发布环境 入口函数
void main() => Application.runApplication(
envTag: EnvTag.pre, // 预发布环境
platform: ApplicationPlatform.app, // 手机应用
baseUrl: 'https://www.wanandroid.com/', // 域名
);
/// 生产环境 入口函数
void main() => Application.runApplication(
envTag: EnvTag.prod, // 生产环境
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, // 异常抛出,会在终端会显示,可帮助开发阶段,快速定位异常所在,但会阻断,后续代码执行
bool pushErrToSentry = false, // 是否开启 异常上报到 Sentry
String? sentryDNS // Sentry DNS 标识
}) {
EnvConfig.envTag = envTag;
EnvConfig.baseUrl = baseUrl;
EnvConfig.platform = platform;
EnvConfig.proxyEnable = proxyEnable;
EnvConfig.caughtAddress = caughtAddress;
EnvConfig.isGlobalNotification = isGlobalNotification;
EnvConfig.throwError = throwError;
EnvConfig.pushErrToSentry = pushErrToSentry;
EnvConfig.sentryDNS = sentryDNS;
/// 确保一些依赖,全部初始化
WidgetsFlutterBinding.ensureInitialized();
/// 初始化路由
Routers.configureRouters();
/// Flutter 框架中 Widget显示错误时,替换为当前组件
ErrorWidget.builder = (FlutterErrorDetails flutterErrorDetails) {
return Material(
child: Center(
child: EnvConfig.envTag == EnvTag.dev
? Text(flutterErrorDetails.exceptionAsString())
: Text(StrCommon.pleaseService),
),
);
};
/// FlutterError.onError 是 Flutter 提供的一个全局错误处理回调,
/// 用于捕获框架内发生的未处理异常。无论是同步还是异步异常,
/// 只要它们发生在 Flutter 框架的上下文中,都会触发这个回调。
FlutterError.onError = (FlutterErrorDetails flutterErrorDetails) async {
if(EnvConfig.pushErrToSentry) {
assert((){
debugPrint('执行了异常 上报:${flutterErrorDetails.exception}');
return true;
}());
/// 使用第三方服务(例如Sentry)上报错误
/// Sentry.captureException(error, stackTrace: stackTrace);
await Sentry.captureException(
flutterErrorDetails.exception,
stackTrace: flutterErrorDetails.stack,
);
}
};
/// runZonedGuarded 捕获 Flutter 框架中的 异步异常
/// 注意:它不是全局的,只能捕获指定范围
runZonedGuarded(() async {
if(EnvConfig.pushErrToSentry) {
/// 初始化 Sentry
await SentryFlutter.init(
(options) {
options.dsn = EnvConfig.sentryDNS;
options.tracesSampleRate = 1.0;
if(EnvConfig.envTag == EnvTag.dev) {
/// 是否打印输出 Sentry 日志
options.debug = true;
}
},
appRunner: () => runApp(App()),
);
} else {
runApp(App());
}
}, (Object error, StackTrace stack) async {
if(EnvConfig.pushErrToSentry) {
assert((){
debugPrint('执行了异常 上报:$error');
return true;
}());
/// 使用第三方服务(例如Sentry)上报错误
/// Sentry.captureException(error, stackTrace: stackTrace);
await Sentry.captureException(
error,
stackTrace: stack,
);
}
});
}
}
在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
class BaseModel<VM extends PageViewModel> {
VM? vm;
void onDispose() {
vm = null;
}
}
abstract class BaseStatefulPage<VM extends PageViewModel> extends BaseViewModelStatefulWidget<VM> {
BaseStatefulPage({super.key});
@override
BaseStatefulPageState<BaseStatefulPage, VM> createState();
}
abstract class BaseStatefulPageState<T extends BaseStatefulPage, VM extends PageViewModel>
extends BaseViewModelStatefulWidgetState<T, VM>
with AutomaticKeepAliveClientMixin {
/// 定义对应的 viewModel
VM? viewModel;
/// 监听应用生命周期
AppLifecycleListener? lifecycleListener;
/// 获取应用状态
AppLifecycleState? get lifecycleState =>
SchedulerBinding.instance.lifecycleState;
/// 是否打印 监听应用生命周期的 日志
bool debugPrintLifecycleLog = false;
/// 进行初始化ViewModel相关操作
@override
void initState() {
super.initState();
/// 初始化页面 属性、对象、绑定监听
initAttribute();
initObserver();
/// 初始化ViewModel,并同步生命周期
viewModel = viewBindingViewModel();
/// 调用viewModel的生命周期,比如 初始化 请求网络数据 等
viewModel?.onCreate();
/// Flutter 低版本 使用 WidgetsBindingObserver,高版本 使用 AppLifecycleListener
lifecycleListener = AppLifecycleListener(
// 监听状态回调
onStateChange: onStateChange,
// 可见,并且可以响应用户操作时的回调
onResume: onResume,
// 可见,但无法响应用户操作时的回调
onInactive: onInactive,
// 隐藏时的回调
onHide: onHide,
// 显示时的回调
onShow: onShow,
// 暂停时的回调
onPause: onPause,
// 暂停后恢复时的回调
onRestart: onRestart,
// 当退出 并将所有视图与引擎分离时的回调(IOS 支持,Android 不支持)
onDetach: onDetach,
// 在退出程序时,发出询问的回调(IOS、Android 都不支持)
onExitRequested: onExitRequested,
);
/// 页面布局完成后的回调函数
lifecycleListener?.binding.addPostFrameCallback((_) {
assert(context != null, 'addPostFrameCallback throw Error context');
/// 初始化 需要context 的属性、对象、绑定监听
initContextAttribute(context);
initContextObserver(context);
});
}
@override
void didChangeDependencies() {
assert((){
debugPrint('BaseStatefulPage.didChangeDependencies --- ${GlobalOperateProvider.getGlobalOperate(context: context)}');
return true;
}());
}
/// 监听状态
onStateChange(AppLifecycleState state) => mLog('app_state:$state');
/// =============================== 根据应用状态产生的各种回调 ===============================
/// 可见,并且可以响应用户操作时的回调
/// 比如从应用后台调度到前台时,在 onShow() 后面 执行
onResume() => mLog('onResume');
/// 可见,但无法响应用户操作时的回调
onInactive() => mLog('onInactive');
/// 隐藏时的回调
onHide() => mLog('onHide');
/// 显示时的回调,从应用后台调度到前台时
onShow() => mLog('onShow');
/// 暂停时的回调
onPause() => mLog('onPause');
/// 暂停后恢复时的回调
onRestart() => mLog('onRestart');
/// 这两个回调,不是所有平台都支持,
/// 当退出 并将所有视图与引擎分离时的回调(IOS 支持,Android 不支持)
onDetach() => mLog('onDetach');
/// 在退出程序时,发出询问的回调(IOS、Android 都不支持)
/// 响应 [AppExitResponse.exit] 将继续终止,响应 [AppExitResponse.cancel] 将取消终止。
Future<AppExitResponse> onExitRequested() async {
mLog('onExitRequested');
return AppExitResponse.exit;
}
/// BaseStatefulPageState的子类,重写 dispose()
/// 一定要执行父类 dispose(),防止内存泄漏
@override
void dispose() {
/// 销毁顺序
/// 1、Model 销毁其持有的 ViewModel
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();
}
/// 2、ViewModel 销毁其持有的 View
/// 3、ViewModel 销毁其持有的 Model
viewModel?.onDispose();
/// 4、View 销毁其持有的 ViewModel
viewModel = null;
/// 5、销毁监听App生命周期方法
lifecycleListener?.dispose();
super.dispose();
}
/// 是否保持页面状态
@override
bool get wantKeepAlive => false;
/// View 持有对应的 ViewModel
VM viewBindingViewModel();
/// 子类重写,初始化 属性、对象
/// 这里不是 网络请求操作,而是页面的初始化数据
/// 网络请求操作,建议在viewModel.onCreate() 中实现
void initAttribute();
/// 子类重写,初始化 需要 context 的属性、对象
void initContextAttribute(BuildContext context) {}
/// 子类重写,初始化绑定监听
void initObserver();
/// 子类重写,初始化需要 context 的绑定监听
void initContextObserver(BuildContext context) {}
/// 输出日志
void mLog(String info) {
if (debugPrintLifecycleLog) {
assert(() {
debugPrint('--- $info');
return true;
}());
}
}
/// 手机应用
Widget appBuild(BuildContext context) => SizedBox();
/// Web
Widget webBuild(BuildContext context) => SizedBox();
/// PC应用
Widget pcBuild(BuildContext context) => SizedBox();
@override
Widget build(BuildContext context) {
/// 使用 AutomaticKeepAliveClientMixin 需要 super.build(context);
///
/// 注意:AutomaticKeepAliveClientMixin 只是保存页面状态,并不影响 build 方法执行
/// 比如 PageVie的 子页面 使用了AutomaticKeepAliveClientMixin 保存状态,
/// PageView切换子页面时,子页面的build的还是会执行
if(wantKeepAlive) {
super.build(context);
}
/// 和 GlobalNotificationWidget,建立依赖关系
if(EnvConfig.isGlobalNotification) {
GlobalNotificationWidget.of(context);
}
switch (EnvConfig.platform) {
case ApplicationPlatform.app: {
if (Platform.isAndroid || Platform.isIOS) {
// 如果,还想根据当前设备屏幕尺寸细分,
// 使用MediaQuery,拿到当前设备信息,进一步适配
return appBuild(context);
}
}
case ApplicationPlatform.web: {
return webBuild(context);
}
case ApplicationPlatform.pc: {
if(Platform.isWindows || Platform.isMacOS) {
return pcBuild(context);
}
}
}
return Center(
child: Text('当前平台未适配'),
);
}
}
/// 基类
abstract class BaseViewModel {
}
/// 页面继承的ViewModel,不直接使用 BaseViewModel,
/// 是因为BaseViewModel基类里代码,还是不要太多为好,扩展创建新的子类就好
abstract class PageViewModel<T extends State> extends BaseViewModel {
/// 定义对应的 view
BaseStatefulPageState? viewState;
/// 使用 直接拿它
T get state => viewState as T;
/// 页面数据model
PageDataModel? pageDataModel;
/// 尽量在onCreate方法中编写初始化逻辑
void onCreate();
/// 对应的widget被销毁了,销毁相关引用对象,避免内存泄漏
void onDispose() {
viewState = null;
pageDataModel = null;
}
/// 请求数据
Future<PageViewModel?> requestData({Map<String, dynamic>? params});
}
/// 内部 有分页列表集合 的实体需要继承 BasePagingModel
class BasePagingModel<VM extends PageViewModel> extends BaseModel{
int? curPage;
List<BasePagingItem>? datas;
int? offset;
bool? over;
int? pageCount;
int? size;
int? total;
BasePagingModel({this.curPage, this.datas, this.offset, this.over,
this.pageCount, this.size, this.total});
}
/// 是分页列表 集合子项 实体需要继承 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;
/// 响应的完整数据
/// 你可能还需要,响应数据的 其他字段,
///
/// assert((){
/// var mListModel = pageDataModel?.data as MessageListModel?; // 转化成对应的Model
/// debugPrint('---pageCount:${mListModel?.pageCount}'); // 获取字段
/// return true;
/// }());
dynamic data;
// 分页参数 字段,一般情况都是固定的,以防万一
String? curPageField;
// 数据列表
List<dynamic> listData = [];
// 当前的PageDataModel
DM? pageDataModel;
// 当前的PageViewModel
VM? pageViewModel;
PagingBehavior pagingBehavior = PagingBehavior.idle;
PagingState pagingState = PagingState.idle;
// 记录之前列表的长度
int originalListDataLength = 0;
PagingDataModel(
{this.curPage = 1,
this.pageCount = 0,
this.total = 0,
this.size = 0,
this.data,
this.curPageField = 'curPage',
this.pageDataModel}) :
listData = [],
originalListDataLength = 0;
/// 这两个方法,由 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 ||
originalListDataLength == listData.length) {
curPage = curPage > 1 ? --curPage : 1;
pagingState = PagingState.loadNoData;
} else {
pagingState = PagingState.loadSuccess;
}
originalListDataLength = listData.length;
} else {
curPage = curPage > 1 ? --curPage : 1;
pagingState = PagingState.loadFail;
}
return pagingState;
}
/// 下拉刷新数据
Future<PagingState> refreshListData() async {
PagingState pagingState = PagingState.curRefreshing;
pagingBehavior = PagingBehavior.refresh;
curPage = 1;
listData.clear();
Map<String, dynamic>? param = {curPageField!: curPage};
PageViewModel? currentPageViewModel = await pageViewModel?.requestData(params: param);
if(currentPageViewModel?.pageDataModel?.type == NotifierResultType.success) {
originalListDataLength = listData.length;
pagingState = PagingState.refreshSuccess;
} else {
originalListDataLength = 0;
pagingState = PagingState.refreshFail;
}
return pagingState;
}
}
安装Sentry,属于服务器相关知识点,本文就不介绍了,网上有很多教程;
我是租了一台云服务,部署的Sentry,完全模拟真实场景,Flutter端这边只需将Sentry生成的
DNS字符串,传入即可。
/// 入口函数
void main() => Application.runApplication(
... ...
pushErrToSentry: true, // 是否开启 异常上报到 Sentry
sentryDNS: 'https://123456789191111@xxx.com/2' // Sentry DNS 标识
/// AndroidManifest.xml 中
/// 开启网络权限
/// <uses-permission android:name="android.permission.INTERNET" />
///
/// 如果使用 http,还需要配置
/// <application
/// android:usesCleartextTraffic="true"
///
// 'http://123456789191111@IP:9000/2';
/// 配置完 域名 和 SSL证书后,http 改为 https,IP 替换为 域名,
// 'https://123456789191111@域名/2';
);
Dio:https://juejin.cn/post/7360227158662807589
路由:https://juejin.cn/post/7294568614203834387
MVVM:https://juejin.cn/post/7166503123983269901
玩Android平台的开放 API;
https://www.wanandroid.com/blog/show/2
这篇文章会 持续更新,后面会加入更多的,通用功能。