这是一款全功能 Flutter 开发脚手架,提供模块化架构与多端适配能力。业务层采用 MVVM 模式,通过 BaseViewModel 实现响应式状态管理及生命周期控制,BaseWidgetPage 统一页面生命周期与 UI 规范。数据层封装网络请求、数据库操作、文件存储及本地缓存,支持加密通信与数据解析。基础组件涵盖屏幕适配、工具类、日志系统、路由管理及事件总线,集成暗黑模式、图片缓存、弹窗队列等实用功能。给你省出一个月的摸鱼时间 ,欢迎点赞交流
业务层
业务层我采取的是借助MVVM(Model-View-ViewModel)模式 ,定义抽象类BaseViewModel
帮我统一管理ViewModel
,定义抽象类BaseWidgetPage
帮我统一管理WidgetPage
的生命周期
BaseViewModel
- 1、数据绑定 通过
ChangeNotifier
+Provider
实现自动响应式更新 - 2、状态管理:内置标准状态机(loading/error/success),统一处理异步操作状态
- 3、生命周期管理:自动处理资源释放,避免内存泄漏
- 4、错误处理中心化 :通过
handleError
统一捕获和处理异常
BaseWidgetPage
- 1、UI管理
- 统一APPBar
- AppBar 标题
- pageID
- buildBody:构建页面主体内容
- 2、声明周期管理
- 1、onPageInit:可以被子类重写的初始化方法
- 2、onPageDispose:可以被子类重写的资源释放方法
- 3、onPageVisible:页面变为可见
- 4、onPageHidden:页面变为隐藏
- 5、didChangeAppLifecycleState:应用回到前台页面可见,应用进入后台页面隐藏
数据层
Flutter跟原生通讯ChannelHandler
MyAppMethodChannelHandler统一 Channel 管理类
MyAppMethodChannelHandler
主要提供两个函数setMethodCallHandler
和callNativeMethod
我还定义了一个APPChannelModel
类,这是一个数据模型类,用于封装从原生代码接收或发送到原生代码的数据。它包含三个属性:code
、message
和 data
,并提供了 fromJson
和 toJson
方法,用于 JSON 数据和 APPChannelModel
对象之间的转换。
使用案例
flutter向原生传值,接收到返回值
php
void _postData() async {
APPChannelModel _model = APPChannelModel(code: "0", message: "传值成功",data: {"one":"1"});
APPChannelModel? _resultModel = await MyAppMethodChannelHandler.callNativeMethod(method: "post_data", model: _model);
print("flutter向原生传值,接收到返回值:${_resultModel.toJson()}");
}
监听原生向flutter发送消息
scss
MyAppMethodChannelHandler.setMethodCallHandler(Router_Page_Method,
(model, method) async {
print(model.toString());
print(method);
});
外观管理
ColorManager
:适配暗黑模式
kotlin
// 定义颜色模式枚举
enum ColorMode {
light,
dark,
}
// 颜色管理类
class ColorManager {}
TextSizeManager
不同屏幕文字大小适配
scss
class TextSizeManager {
// 设计稿基准宽度,根据实际设计稿修改
static const double baseWidth = 375;
// 根据设备宽度计算适配后的文字大小
static double getAdaptiveTextSize(BuildContext context, double originalSize) {
// 获取当前设备的屏幕宽度
double screenWidth = MediaQuery.of(context).size.width;
// 计算缩放比例
double scale = screenWidth / baseWidth;
// 返回适配后的文字大小
return originalSize * scale;
}
// 提供不同字号的获取方法
static double getSmallTextSize(BuildContext context) {
return getAdaptiveTextSize(context, 12);
}
static double getMediumTextSize(BuildContext context) {
return getAdaptiveTextSize(context, 16);
}
static double getLargeTextSize(BuildContext context) {
return getAdaptiveTextSize(context, 20);
}
}
底座业务组件
1、基于dio的网络封装
实现功能
- 1、get、post请求
- 2、自定义RequestOptions
- 3、dio请求管理队列,用于统一管理请求
- 4、HttpClient链接管理,用于获取解析DNS时间、TCP连接开始时间、SSL握手开始时间(如果是HTTPS)、首包时间
- 5、json转model
- 6、缓存管理
- 7、日志管理拦截器
- 8、数据转换管理拦截器
- 9、loading拦截器
- 10、token续租拦截器
- 11、错误处理拦截器
参考文章:Flutter dio 手把手教你封装一个实用网络工具
2、基于cached_network_image图片缓存库
1、CachedImageWidget图片缓存widget
bash
CachedImageWidget(imageUrl: _imageUrl, onSuccess: (image,iconUrl){
print("图片下载成功:${image},=====${iconUrl}");
},onError: (error,iconUrl){
print("图片下载失败:${error},=====${iconUrl}");
}),
2、MyCacheImageManager图片缓存管理
-
1、指定缓存目录,缓存有效期、最大缓存数量
lessMyCustomCacheManager._() : super(Config( key, stalePeriod: const Duration(days: 30), // 缓存有效期 maxNrOfCacheObjects: 100, // 最大缓存数量 repo: JsonCacheInfoRepository(databaseName: key), ));
-
2、
getFilePath(String imageUrl)
获取本地目录rust/// 获取图片本地路径 static Future<String?> getFilePath(String imageUrl) async { final FileInfo? fileInfo = await _cacheManager.getFileFromCache(imageUrl); return fileInfo?.file.path; }
-
3、
clearImageCache(String imageUrl)
移除指定路径下图片dart/// 移除指定路径下图片 static Future<void> clearImageCache(String imageUrl) async { // 移除单个文件的缓存 try { await _cacheManager.removeFile(imageUrl); print(' 移除指定路径下图片已成功移除'); } catch (e) { print(' 移除指定路径下图片缓存时出错: $e'); } }
-
4、
clearAllCache()
移除所有图片dart/// 移除所有图片 static Future<void> clearAllCache() async { try { await _cacheManager.emptyCache(); print('移除所有图片缓存已成功移除'); } catch (e) { print('移除所有图片缓存时出错: $e'); } }
-
5、
getCacheSize()
获取缓存大小arduino/// 获取缓存大小 static Future<String> getCacheSize() async { int size = await _cacheManager.store.getCacheSize(); double cacheSize = size / 1024 / 1024; return cacheSize.toStringAsFixed(2); }
3、ToastUtil
-
1、showToast:普通提示信息
php/// 提示信息 static void showToast( {required String msg, int duration = 2000, bool dismissOnTap = false}) { EasyLoading.showToast(msg, duration: Duration(milliseconds: duration), toastPosition: EasyLoadingToastPosition.center, dismissOnTap: dismissOnTap); }
-
2、showLoading:loading加载框
ini/// 加载框 static void showLoading({String? msg, bool dismissOnTap = false}) { EasyLoading.instance ..indicatorType = EasyLoadingIndicatorType.ring ..loadingStyle = EasyLoadingStyle.dark ..radius = 5.0 ..maskColor = Colors.white.withOpacity(0.1); EasyLoading.show( status: msg, maskType: EasyLoadingMaskType.custom, dismissOnTap: dismissOnTap); }
-
3、dismiss:隐藏loading
csharp/// 隐藏loading static void dismiss() { if (EasyLoading.isShow) { EasyLoading.dismiss(animation: true); } }
4、弹窗队列管理器
弹窗类型枚举
scss
// 弹窗类型枚举
enum DialogType {
center, // 中间弹窗
bottom, // 底部弹窗
}
弹窗队列实现
less
// 添加弹窗到队列
void add({
required BuildContext context,
required WidgetBuilder builder,
DialogType type = DialogType.center,
VoidCallback? onDismiss,
Color? backgroundColor, // 底部弹窗专用参数
ShapeBorder? shape, // 底部弹窗专用参数
}) {
_queue.add(DialogConfig(
context: context,
builder: builder,
type: type,
onDismiss: onDismiss,
backgroundColor: backgroundColor,
shape: shape,
));
_checkNext();
}
使用案例
less
void _showQueueDiaLog() {
// // 在任意位置添加弹窗
DialogQueue().add(
context: context,
builder: (context) => AlertDialog(
title: const Text('提示1'),
content: const Text('这是第一个弹窗'),
actions: [
TextButton(
child: const Text('关闭'),
onPressed: () => Navigator.pop(context),
),
],
),
onDismiss: () => print('第一个弹窗关闭'),
);
// 添加底部弹窗
DialogQueue().add(
context: context,
type: DialogType.bottom,
builder: (_) => CustomBottomSheetContent(),
backgroundColor: Colors.grey[100],
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(30))));
DialogQueue().add(
context: context,
builder: (BuildContext context) {
return const CustomDialog();
},
type: DialogType.center,
onDismiss: () {
print('自定义弹窗已关闭');
},
);
}
5、刷新组件
刷新组件基于pull_to_refresh_flutter3
封装,支持onRefresh和onLoading回调,是否启用上拉加载,以及子内容。
less
// 封装的刷新组件
class CustomRefreshWidget<T> extends StatelessWidget {
final RefreshController controller;
final Future<void> Function() onRefresh;
final Future<void> Function()? onLoading;
final List<T> dataList;
final Widget Function(BuildContext context, int index) itemBuilder;
const CustomRefreshWidget({
Key? key,
required this.controller,
required this.onRefresh,
this.onLoading,
required this.dataList,
required this.itemBuilder,
}) : super(key: key);
Widget headerBuilder(BuildContext context, RefreshStatus? mode) {
Widget body;
if (mode == RefreshStatus.idle) {
body = const Text("下拉刷新", style: TextStyle(fontSize: 16));
} else if (mode == RefreshStatus.refreshing) {
body = const CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.grey),
);
} else if (mode == RefreshStatus.canRefresh) {
body = const Text("释放立即刷新", style: TextStyle(fontSize: 16));
} else if (mode == RefreshStatus.completed) {
body = const Text("刷新完成", style: TextStyle(fontSize: 16));
} else if (mode == RefreshStatus.failed) {
body = const Text("刷新失败", style: TextStyle(fontSize: 16));
} else {
body = const Text("未知状态", style: TextStyle(fontSize: 16));
}
return Container(
height: 80.0,
alignment: Alignment.center,
color: Colors.white, // 设置背景颜色
child: body,
);
}
Widget footerBuilder(BuildContext context, LoadStatus? mode) {
Widget body;
if (mode == LoadStatus.idle) {
body = const Text(
"上拉加载",
style: TextStyle(fontSize: 16),
);
} else if (mode == LoadStatus.loading) {
body = const CircularProgressIndicator();
} else if (mode == LoadStatus.failed) {
body = const Text("加载失败!点击重试!", style: TextStyle(fontSize: 16));
} else if (mode == LoadStatus.canLoading) {
body = const Text("释放加载更多", style: TextStyle(fontSize: 16));
} else {
body = const Text("没有更多数据了", style: TextStyle(fontSize: 16));
}
return SizedBox(
height: 55.0,
child: Center(child: body),
);
}
@override
Widget build(BuildContext context) {
return RefreshConfiguration(
springDescription:
const SpringDescription(stiffness: 200, damping: 20, mass: 2.0),
// 调整弹簧动画属性
maxOverScrollExtent: 80,
// 减少最大下拉距离
maxUnderScrollExtent: 0,
enableScrollWhenRefreshCompleted: true,
enableLoadingWhenFailed: true,
hideFooterWhenNotFull: false,
enableBallisticLoad: true,
child: SmartRefresher(
controller: controller,
enablePullDown: true,
enablePullUp: onLoading != null,
header: CustomHeader(builder: headerBuilder),
footer: CustomFooter(builder: footerBuilder),
onRefresh: onRefresh,
onLoading: onLoading,
child: ListView.builder(
itemCount: dataList.length,
itemBuilder: itemBuilder,
),
),
);
}
}
底座数据层
1、数据库
1、db封装
1、插入数据
python
/*
int id = await dbHelper.insert({'name': 'Alice'}, 'my_table');
print('Inserted with ID: $id');
* */
// 插入数据
Future<int> insert(Map<String, dynamic> row, String tableName) async {
Database db = await database;
return await db.insert(tableName, row);
}
2、 查询所有数据
swift
/*
List<Map<String, dynamic>> allRows = await dbHelper.queryAll('my_table');
print('All rows: $allRows');
* */
// 查询所有数据
Future<List<Map<String, dynamic>>> queryAll(String tableName) async {
Database db = await database;
return await db.query(tableName);
}
3、根据条件查询数据
vbnet
/*
// 根据条件查询数据
List<Map<String, dynamic>> filteredRows = await dbHelper.query(
'my_table',
where: 'name = ?',
whereArgs: ['Alice'],
);
print('Filtered rows: $filteredRows');
* */
// 根据条件查询数据
Future<List<Map<String, dynamic>>> query(String tableName,
{String? where,
List<dynamic>? whereArgs,
String? orderBy,
int? limit,
int? offset}) async {
Database db = await database;
return await db.query(
tableName,
where: where,
whereArgs: whereArgs,
orderBy: orderBy,
limit: limit,
offset: offset,
);
}
4、更新数据
vbnet
/*
int updatedRows = await dbHelper.update(
'my_table',
{'name': 'Bob'},
'id = ?',
whereArgs: [id],
);
print('Updated $updatedRows rows');
* */
// 更新数据
Future<int> update(String tableName, Map<String, dynamic> row, String where,
{List<dynamic>? whereArgs}) async {
Database db = await database;
return await db.update(
tableName,
row,
where: where,
whereArgs: whereArgs,
);
}
5、删除数据
vbnet
/*
int deletedRows = await dbHelper.delete(
'my_table',
'id = ?',
whereArgs: [id],
);
print('Deleted $deletedRows rows');
* */
// 删除数据
Future<int> delete(String tableName, String where,
{List<dynamic>? whereArgs}) async {
Database db = await database;
return await db.delete(
tableName,
where: where,
whereArgs: whereArgs,
);
}
6、关闭数据库
csharp
// 关闭数据库
Future close() async {
Database db = await database;
return db.close();
}
2、file文件读写
1、写入文件
dart
/// 写入文件
Future<void> writeFile({required String fileName,required String content, String? moduleName}) async {
try {
final file = await _localFile(fileName: fileName, moduleName: moduleName);
Log.debug("文件地址:${file.path}");
// 等待写入操作完成
await file.writeAsString(content);
} on PlatformException catch (e) {
Log.debug('写入文件时发生平台异常: ${e.message}');
rethrow;
} on FileSystemException catch (e) {
Log.debug('文件系统写入出错: ${e.message}');
rethrow;
} catch (e) {
Log.debug('文件写入失败: $e');
rethrow;
}
}
2、追加内容到文件
dart
// 追加内容到文件
Future<void> appendToFile({required String fileName,required String content, String? moduleName}) async {
try {
final file = await _localFile(fileName: fileName, moduleName: moduleName);
// 以追加模式写入内容
await file.writeAsString(content, mode: FileMode.append);
Log.debug('内容已成功追加到文件: ${file.path}');
} on PlatformException catch (e) {
Log.debug('追加内容时发生平台异常: ${e.message}');
rethrow;
} on FileSystemException catch (e) {
Log.debug('文件系统操作出错: ${e.message}');
rethrow;
} catch (e) {
Log.debug('追加内容到文件时出现未知错误: $e');
rethrow;
}
}
3、读取文件
dart
/// 读取文件
Future<String?> getFile({required String fileName, String? moduleName}) async {
try {
final file = await _localFile(fileName: fileName, moduleName: moduleName);
Log.debug("读取文件路径:${file.path}");
String contents = await file.readAsString();
return contents;
} on PlatformException catch (e) {
Log.debug('读取文件时发生平台异常: ${e.message}');
return null;
} on FileSystemException catch (e) {
Log.debug('文件系统读取出错: ${e.message}');
return null;
} catch (e) {
Log.debug('文件读取失败: $e');
return null;
}
}
4、移除指定文件
dart
/// 移除指定文件
Future<bool> removeFilePath({required String fileName, String? moduleName}) async {
try {
final file = await _localFile(fileName: fileName,moduleName: moduleName);
// 检查文件是否存在
if (await file.exists()) {
// 移除文件
await file.delete();
Log.debug('文件删除成功: ${file.path}');
return true;
} else {
Log.debug('文件不存在,无需删除: ${file.path}');
return false;
}
} on PlatformException catch (e) {
Log.debug('删除文件时发生平台异常: ${e.message}');
return false;
} on FileSystemException catch (e) {
Log.debug('文件系统删除出错: ${e.message}');
return false;
} catch (e) {
Log.debug('移除文件时出现未知错误: $e');
return false;
}
}
3、基于shared_preferences封装小数据读写
scss
class PreferencesHelper {
/// 异步设置字符串值
static Future<void> setString(String key, String value) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setString(key, value);
}
/// 异步获取字符串值,带默认值
static Future<String?> getString(String key) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String? value = prefs.getString(key);
return value;
}
/// 异步设置整数值
static Future<void> setInt(String key, int value) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setInt(key, value);
}
/// 异步获取整数值,带默认值
static Future<int?> getInt(String key) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
int? value = prefs.getInt(key);
return value;
}
/// 异步设置布尔值
static Future<void> setBool(String key, bool value) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setBool(key, value);
}
/// 异步获取布尔值,带默认值
static Future<bool?> getBool(String key) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
bool? value = prefs.getBool(key);
return value;
}
/// 异步设置双精度浮点数值
static Future<void> setDouble(String key, double value) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setDouble(key, value);
}
/// 异步获取双精度浮点数值,带默认值
static Future<double?> getDouble(String key) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
double? value = prefs.getDouble(key);
return value;
}
/// get keys.
/// 获取sp中所有的key
static Future<Set<String>> getKeys() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.getKeys();
}
/// remove.
/// 移除sp中key的值
static Future<bool> remove(String key) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
return await prefs.remove(key);
}
/// 清除所有键值对
static Future<void> clear() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.clear();
}
}
2、jsonConverter类型解析失败问题
主要扩展了下面几个类型转换器
- 1、JsonTypeAdapter
- 2、SafeConvertModel
- 3、SafeDateTimeConverter
- 4、SafeListConverter
- 5、SafeMapConverter
- 6、SafeNumConverter
参考文章:Flutter 一招教你解决json_annotation类型解析失败问题
3、日志Log
日志Log是基于logger
封装实现的,主要实现一下功能
- 多级别日志支持 :
- Verbose (详细)
- Debug (调试)
- Info (信息)
- Warning (警告)
- Error (错误)
- WTF (严重错误)
- 配置选项 :
- 控制堆栈跟踪显示行数
- 自定义输出颜色
- 设置日志行长度
- 显示打印时间
- 表情符号开关
- 性能优化 :
- 支持全局日志开关
- 生产环境默认过滤敏感信息
- 自动处理对象转字符串
- 错误处理 :
- 支持携带错误对象
- 可记录堆栈跟踪信息
- 错误日志显示更多上下文
kotlin
/// 日志配置选项
class LogOptions {
final int methodCount;
final int errorMethodCount;
final int lineLength;
final bool colors;
final bool printEmojis;
final bool printTime;
LogOptions({
this.methodCount = 0,
this.errorMethodCount = 8,
this.lineLength = 120,
this.colors = true,
this.printEmojis = true,
this.printTime = false,
});
}
4、crypto加解密封装
- 1、MD5 加密
- 2、SHA-256 加密
- 3、AES 加密解密
5、router路由组件
1、跳转工具类
1、路由跳转
dart
static Future<T?> router<T extends Object?>(
{required RouterURL routerURL,
required BuildContext context,
Map<String, dynamic>? param,
MyRouterEnum routerType = MyRouterEnum.push}) {
final name = routerURL.name;
Map<String, dynamic> queryParameters = param ?? Map<String, dynamic>();
if (routerType == MyRouterEnum.push) {
return context.pushNamed(name, queryParameters: queryParameters);
} else {
context.goNamed(name, queryParameters: queryParameters);
return Future.value();
}
}
2、pop 返回
dart
static void pop<T extends Object?>(BuildContext context, [T? result]) {
if (context.canPop()) {
context.pop(result);
} else {
assert(false, '不能pop');
}
}
3、返回到指定界面
ini
static void popUntil(
{required BuildContext context, required RouterURL routerURL}) {
try {
List<Route<dynamic>> list = getAllRoutes();
bool isCanPop = false;
for (Route _router in list) {
if(_router.settings.name == routerURL.name) {
isCanPop = true;
}
}
if (isCanPop) {
final name = routerURL.name;
Navigator.popUntil(context, ModalRoute.withName(name));
} else {
assert(false, '不能pop');
}
} catch (e) {
Log.error("返回到指定界面错误:${e.toString()}");
}
}
4、获取当前路由栈里面的全部路由
swift
/// 获取当前路由栈里面的全部路由
static List<Route<dynamic>> getAllRoutes() {
final MyRouteObserver routeObserver = MyRouteObserver();
List<Route<dynamic>> routes = routeObserver.routeStack;
return routes;
}
2、添加路由观察者
scss
class MyRouteObserver extends NavigatorObserver {
static final MyRouteObserver _instance = MyRouteObserver._internal();
factory MyRouteObserver() {
return _instance;
}
MyRouteObserver._internal();
final List<Route<dynamic>> routeStack = [];
final Map<Route<dynamic>, List<RouteAware>> _routeAwareSubscriptions = {};
/// 订阅路由变化
void subscribe(RouteAware routeAware, Route<dynamic> route) {
_routeAwareSubscriptions.putIfAbsent(route, () => []).add(routeAware);
}
/// 取消订阅路由变化
void unsubscribe(RouteAware routeAware) {
for (final route in _routeAwareSubscriptions.keys) {
_routeAwareSubscriptions[route]?.remove(routeAware);
}
}
/// 当一个新的路由被推送到导航栈时,此方法会被调用。
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
super.didPush(route, previousRoute);
routeStack.add(route);
Log.debug(
'新的路由被推送到导航栈: ${route.settings.name} param:${route.settings.arguments}, previousRoute= ${previousRoute?.settings.name}');
_handleRouteVisibility(previousRoute, route);
}
/// 当一个路由从导航栈中弹出时,此方法会被调用。route 参数表示被弹出的路由,previousRoute 参数
@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
super.didPop(route, previousRoute);
routeStack.remove(route);
Log.debug(
'路由被弹出,当前路由堆栈: ${route.settings.name},param:${route.settings.arguments}, previousRoute= ${previousRoute?.settings.name}');
_handleRouteVisibility(route, previousRoute);
}
/// 当一个路由从导航栈中被移除时,此方法会被调用。移除路由和弹出路由不同,移除操作可以移除导航栈中任意位置的路由,而弹出操作只能移除栈顶的路由。
/// route 参数表示被移除的路由,previousRoute 参数表示在该路由移除后,其下一个路由(如果存在的话)。
@override
void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) {
super.didRemove(route, previousRoute);
routeStack.remove(route);
Log.debug(
'路由被移除,当前路由堆栈: ${route.settings.name}, previousRoute= ${previousRoute?.settings.name}');
_handleRouteVisibility(route, previousRoute);
}
@override
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {
super.didReplace(newRoute: newRoute, oldRoute: oldRoute);
if (oldRoute != null) {
routeStack.remove(oldRoute);
}
if (newRoute != null) {
routeStack.add(newRoute);
}
Log.debug(
'路由被替换,当前路由堆栈: new= ${newRoute?.settings.name}, old= ${oldRoute?.settings.name}');
_handleRouteVisibility(oldRoute, newRoute);
}
/// 当用户开始进行一个导航手势(如在 iOS 上从屏幕边缘向左滑动返回上一页)时,此方法会被调用。
/// route 参数表示当前正在操作的路由,previousRoute 参数表示在手势操作后可能会显示的前一个路由(如果存在的话)。
@override
void didStartUserGesture(
Route<dynamic> route, Route<dynamic>? previousRoute) {
Log.debug('手势事件 didStartUserGesture: ${route.settings.name}, '
'previousRoute= ${previousRoute?.settings.name}');
}
/// 用户结束导航手势时,此方法会被调用。无论手势是否成功完成导航操作,只要手势结束,就会触发这个方法。
@override
void didStopUserGesture() {
Log.debug('手势结束:didStopUserGesture');
}
/// 处理路由可见性变化
void _handleRouteVisibility(
Route<dynamic>? oldRoute, Route<dynamic>? newRoute) {
if (oldRoute != null) {
_notifyRouteAware(oldRoute, false);
}
if (newRoute != null) {
_notifyRouteAware(newRoute, true);
}
}
/// 通知订阅者路由可见性变化
void _notifyRouteAware(Route<dynamic> route, bool isVisible) {
final routeAwares = _routeAwareSubscriptions[route];
if (routeAwares != null) {
for (final routeAware in routeAwares) {
if (isVisible) {
routeAware.didPush();
} else {
routeAware.didPopNext();
}
}
}
}
}
3、定义路由映射url
dart
/// 路由映射url
class RouterURL {
/// 名称
final String name;
/// 路径
final String path;
const RouterURL({required this.name, required this.path});
}
6、eventBus封装
- 1、单例模式 :全局唯一事件总线实例,通过
AppEventBus.instance
访问核心功能 - 2、类型安全:强类型事件处理,编译时类型检查
- 3、生命周期管理 :
- 通过
EventBusMixin
自动取消订阅 - 手动订阅返回
StreamSubscription
便于管理
- 通过
- 4、安全防护
- 异常捕获机制防止事件处理崩溃
- 错误处理回调支持
dart
// 封装后的高级事件总线
class AppEventBus {
static final EventBus _instance = EventBus();
// 私有构造,确保单例
AppEventBus._internal();
/// 获取单例实例
static EventBus get instance => _instance;
/// 发送事件
static void sendEvent<T>(T event) {
if (kDebugMode) {
print('[EventBus] Firing event: ${event.runtimeType}');
}
instance.fire(event);
}
/// 订阅事件,返回可取消的订阅对象
static StreamSubscription<T> on<T>(void Function(T event) handler, {
bool handleError = true,
ErrorCallback? onError,
}) {
final subscription = instance.on<T>().listen((event) {
if (kDebugMode) {
print('[EventBus] Received event: ${event.runtimeType}');
}
_safeRun(() => handler(event), onError: onError);
}, onError: handleError ? (error, stack) {
_safeRun(() => onError?.call(error, stack));
} : null);
return subscription;
}
static void _safeRun(void Function() action, {ErrorCallback? onError}) {
try {
action();
} catch (e, s) {
if (kDebugMode) {
print('[EventBus] Handler error: $e\n$s');
}
onError?.call(e, s);
}
}
}
/// Flutter Widget 集成扩展
mixin EventBusMixin<T extends StatefulWidget> on State<T> {
final List<StreamSubscription> _eventSubscriptions = [];
/// 安全订阅事件,自动管理生命周期
void subscribe<Event>(void Function(Event event) handler, {
bool handleError = true,
ErrorCallback? onError,
}) {
_eventSubscriptions.add(
AppEventBus.on<Event>(handler, handleError: handleError, onError: onError)
);
}
@override
void dispose() {
for (final sub in _eventSubscriptions) {
sub.cancel();
}
if (kDebugMode) {
print('[EventBus] Canceled ${_eventSubscriptions.length} subscriptions');
}
super.dispose();
}
}
typedef ErrorCallback = void Function(Object error, StackTrace stackTrace);
7、Stream结合RxDart封装
Stream可以简单的处理数据流,但遇到更复杂的需求时,发现原生Stream的操作符不够用。这个时候我们就可以借助于RxDart。RxDart可以提供更多的操作符的链式调用、错误处理、流的组合。
csharp
class RxStream<T> {
final BehaviorSubject<T> _subject = BehaviorSubject<T>();
Stream<T> get stream => _subject.stream;
// 添加数据
void add(T value) => _subject.sink.add(value);
// 链式操作符示例:防抖 + 过滤空值
Stream<T> debounceAndFilter(Duration duration) {
return stream
.debounceTime(duration) // 防抖
.where((value) => value != null); // 过滤空值
}
// 合并多个流(例如:搜索输入 + 筛选条件)
static Stream<R> combineStreams<A, B, R>(
Stream<A> streamA,
Stream<B> streamB,
R Function(A, B) combiner,
) {
return Rx.combineLatest2(streamA, streamB, combiner);
}
// 关闭资源
void dispose() => _subject.close();
}
参考文章:flutter 流(Stream)介绍&结合RxDart使用
底座基础组件
1、布局类
ScreenAdapter屏幕适配
arduino
class ScreenAdapter {
// 初始化屏幕适配
static void init(BuildContext context, {double width = 375, double height = 812}) {
ScreenUtil.init(
context,
designSize: Size(width, height),
);
}
// 获取屏幕宽度
static double get screenWidth => ScreenUtil().screenWidth;
// 获取屏幕高度
static double get screenHeight => ScreenUtil().screenHeight;
// 获取状态栏高度
static double get statusBarHeight => ScreenUtil().statusBarHeight;
// 获取底部安全区高度
static double get bottomBarHeight => ScreenUtil().bottomBarHeight;
// 适配宽度
static double setWidth(double width) {
return width.w;
}
// 适配高度
static double setHeight(double height) {
return height.h;
}
// 适配字体大小
static double setSp(double fontSize) {
return fontSize.sp;
}
}
IntScreenExtensions&DoubleScreenExtensions
dart
// 为 int 类型添加扩展
extension IntScreenExtensions on int {
/// 转换为适配后的像素值
double get px => toDouble().w;
/// 转换为适配后的响应式像素值(这里使用与 px 相同逻辑,可按需调整)
double get rpx => toDouble().w;
}
// 为 double 类型添加扩展
extension DoubleScreenExtensions on double {
/// 转换为适配后的像素值
double get px => w;
/// 转换为适配后的响应式像素值(这里使用与 px 相同逻辑,可按需调整)
double get rpx => w;
}
2、utils工具
date_untils时间工具
- 1、获取当前时间戳(毫秒)
- 2、获取当前时间
- 3、将某个格式时间转化成时间戳(毫秒)
- 4、将某个格式时间转化为指定格式时间
- 5、获取当前年、月、日
DoubleExtension
- 1、保留指定小数位数
StringExtension
- 1、md5加密
- 2、将字符串转换为 Map<String, dynamic>
- 3、Base64 编码、Base64 解码
- 4、截取指定长度字符串
- 5、 按索引范围替换字符串
ExtensionList
- 1、 将list转化为json字符串
- 2、判断对象是否为null
- 3、字符串拼接
- 4、列表元素去重
ExtensionMap
- 1、将map转化为json字符串
- 2、合并两个 Map
- 3、筛选符合条件的键值对
- 4、将 Map 的值转换为另一种类型
- 5、获取 Map 中第一个满足条件的键值对
ColorExtension
- 1、十六进制颜色设置
- 2、 hex颜色设置
- 3、取随机颜色
WidgetExtension
- 1、切圆角
- 2、点击事件
- 3、 长按事件
ImageExtension
- 1、根据指定的角度旋转图片
- 2、图片灰度
TextFieldExtension
-
1、自动获取焦点
-
2、限制长度
-
3、手机号格式化
JsonUtils
-
1、将 Map 转换为 String
-
2、 将 String 转换为 Map
-
3、 将 List 转换为 String
-
4、将 String 转换为 List