# flutter-demo **Repository Path**: lhc542/flutter-demo ## Basic Information - **Project Name**: flutter-demo - **Description**: flutter - demo - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2025-09-03 - **Last Updated**: 2026-03-10 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # demo flutter test demo # 图片资源 ## 多倍图 生成 1,2 倍图 用vscode插件 Flutter GetX Generator ## 资源代码生成 - [ ] [flutter_gen](https://pub.dev/packages/flutter_gen) - [x] [spider](https://pub.dev/packages/spider) spider 配置更简单,支持类名/输出路径/命名前缀/修改,能将多个文件夹输出到同一个类 配置: ```yaml # 在lib文件夹的哪里生成dart代码? package: common/values # 放一个文件里,根据前缀区分类型 - class_name: R sub_groups: - path: assets/images #前缀 prefix: img types: [ .png, .jpg, .jpeg, .webp, .webm, .bmp] - path: assets/svgas prefix: svga types: [ .svga ] ``` ![image-20250815165208975](assets/image-20250815165208975.png) 控制台执行:spider build 输出结果: ```dart class R { R._(); static const String imgMyAccountHead = 'assets/images/my_account_head.png'; static const String svgaCoins = 'assets/svgas/coins.svga'; } ``` # 状态管理 - [x] [getx](https://pub.dev/packages/get) ## 插件 - Android Studio [GetX代码生成IDEA插件](https://juejin.cn/post/7005003323753365517) - vs code : Flutter GetX Generator,GetX Snippets ## 代码编写建议 [Flutter 改善套娃地狱问题](https://juejin.cn/post/6939774499399139336) ## 独立控制器实例 某些情况下,多个相同页面可能需要独立的控制器 引用[3.24.0更新说明](https://github.com/jonataslaw/getx/blob/daa3b93/CHANGELOG.md)对GetWidget的描述 > 您可以使用 Get.put / Get.lazyput 来处理全局依赖,并使用 Get.create 和 GetWidget 来处理临时依赖 但是根据[Issue #1059](https://github.com/jonataslaw/getx/issues/1059) 作者在2月份回复:GetWidget有点奇怪,我可能会停止使用它。保持关注此问题... **注意**:create会导致find每次都获取新的实例,无法再获取全局实例,GetWidget会缓存实例避免重复获取,子组件需要通过构造函数等方法传递 举个例子: ```dart GetPage( name: RouteNames.test, page: () => TestPage(), binding: BindingsBuilder(() { Get.create(() => TestCtrl()); }), ), class TestPage extends GetWidget { TestPage({Key? key}) : super(key: key); static open() { Get.toNamed(RouteNames.test, preventDuplicates: false); } @override Widget build(BuildContext context) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( color: Colors.blue, width: 100.w, height: 20.w, child: GetBuilder( init: ctrl, global: false, builder: (ctrl) { return Text(ctrl.va.value.toString()); }, ), ).onTap(() { ctrl.va.value++; }), Text("跳转").onTap(TestPage.open), ], ), ); } } ``` # 屏幕适配 - [x] [flutter_screenutil](https://pub.dev/packages/flutter_screenutil) # 网络请求 - [x] [dio](https://pub.dev/packages/dio) ## 关联Controller取消 ```dart ///接口写这里 mixin ApiMixin on GetxController { final CancelToken cancelToken = CancelToken(); @override void onClose() { cancelToken.cancel("onClose 请求取消"); super.onClose(); } Future loginGuest() async { Map params = { // todo 补充参数 }; HttpResponseData? data = await HttpUtil().post( url: "/mobile.login/guest", params: params, cancelToken: cancelToken, showErrorToast: true //报错是否显示toast ); return LoginData.fromJson(data?.jieguo); } } ``` ApiMixin 内处理接口的传参和返回值,并能拿到关联控制器的CancelToken 不需要自动取消的请求可以不传入CancelToken,或者在持久化的控制器中调用 比如:Get.put(AppCtrl(), permanent: true); ## 使用 ```dart testApi() async { /// 执行前后增加loading loadingScope(() async { var data = await loginGuest(); // 如果需要中途关闭loading 执行DialogUtils.dismissLoading(); Log.debug(data); }, onError: (e) { //已处理日志打印 }); /// 处理接口异常 try { LoginData data = await loginGuest(); } catch (e, st) { Log.handle(e, st); } /// 如果异常返回null LoginData? loginData = await safeAwait(loginGuest()); /// 回调的形式 loginGuest() .then((value) { Log.debug(value); }) .onError((error, stackTrace) { Log.handle(error, stackTrace); }); } ``` ## json解析类型不匹配 后端的值可能是这样 "0" 或者 0 摇摆不定发生报错 参考[Flutter 一招教你解决json_annotation类型解析失败问题](https://juejin.cn/post/7472785299732496418) 增加了SafeDoubleConverter,SafeIntConverter,SafeStringConverter对数据进行强转,如果失败返回默认值 json_serializable 可以对整个类添加转换器 ``` @JsonSerializable(converters: [SafeStringConverter()]) ``` freezed 只能对字段添加。。。 在使用的字段上增加注解,例如: ```dart @JsonKey(name: "xinyonghu") @SafeIntConverter() required int xinyonghu, /// 生成后的代码会通过Converter xinyonghu: const SafeIntConverter().fromJson(json['xinyonghu']), ``` ## 日志记录 - [x] [talker ](https://pub.dev/packages/talker) 支持路由跳转,dio,颜色配置,提供当独查看的小组件 ```dart // 已在内部处理StackTrace行号显示 Log.w(data); Log.d(data); Log.i(data); Log.e(data); Log.h(data); ``` # dialog - [x] [SmartDialog](https://pub.dev/packages/flutter_smart_dialog) 与页面路由区分开来,支持绑定组件和页面 ## 单个显示 ```dart ///任意组件可显示 Container().showDialog( alignment: Alignment.center, // 显示位置 默认Alignment.center backDismiss: false, // 返回是否关闭 默认false clickMaskDismiss: false,// 点击遮罩是否关闭 默认false bindWidget: null, // 绑定widget 适合用于绑定PageView子页面 onDismiss: null,//关闭回调 tag: null, // tag 关闭可指定tag ); ``` ## 连续弹窗 用来连续显示dialog 先按权重高的优先显示 再按先加入的优先显示 测试案例 结果 5-1-3-2-4 ```dart DialogList dialogList = DialogList(); dialogList.add( GestureDetector( onTap: DialogUtils.dismiss, child: Container(color: Colors.yellow, width: 100, height: 100, child: Text("1")), ), ); dialogList.add( GestureDetector( onTap: DialogUtils.dismiss, child: Container(color: Colors.yellow, width: 100, height: 100, child: Text("2")), ), weight: 88, ); dialogList.add( GestureDetector( onTap: DialogUtils.dismiss, child: Container(color: Colors.yellow, width: 100, height: 100, child: Text("3")), ), weight: 99, ); dialogList.add( GestureDetector( onTap: DialogUtils.dismiss, child: Container(color: Colors.yellow, width: 100, height: 100, child: Text("4")), ), weight: 88, ); dialogList.add( GestureDetector( onTap: DialogUtils.dismiss, child: Container(color: Colors.yellow, width: 100, height: 100, child: Text("5")), ), force: true, ); ``` # SharedPreferences 将key统一管理,支持动态后缀参数 SpValue负责限制读/写的类型,需要传入默认值。 SpObj负责对象类型需要传入json转换函数,默认值是null(可传入)。 ## 使用 "token".spValue("") 这里的token 会作为key,建议和属性名一致避免重复 ```dart class SpKeys { static SpValue token = SpValue(key: "token", defaultValue: ""); // 测试默认值 static SpValue test = SpValue(key: "token", defaultValue: 1); //测试后缀 要小心key没有和其他key发生重叠 static SpValue testSuffix = SpValue(key: "testSuffix", defaultValue: 0, defaultSuffix: "1"); static SpObj loginData = SpObj("loginData", fromJson: loginDataFromJson, toJson: loginDataToJson); } test() async { Log.d("token=${SpKeys.token.getValue()}"); //显示"" Log.d("test=${SpKeys.test.getValue()}"); //显示"1" Log.d("loginData=${SpKeys.loginData.getValue()}"); //显示"null" await SpKeys.token.setValue("testToken"); await SpKeys.test.setValue(2); await SpKeys.loginData.setValue(LoginData(yonghuId: 0, pingju: "pingju111", xinyonghu: 221)); Log.d("token=${SpKeys.token.getValue()}"); Log.d("test=${SpKeys.test.getValue()}"); Log.d("loginData=${SpKeys.loginData.getValue()}"); //改变后缀 SpKeys.testSuffix.setValue(1); SpKeys.testSuffix.defaultSuffix = "2"; SpKeys.testSuffix.setValue(4); Log.d("testSuffix=${SpKeys.testSuffix.getAll()}"); await SpKeys.testSuffix.removeAll(); Log.d("testSuffix=${SpKeys.testSuffix.getAll()}"); } ``` # 缺省页 ## 配置全局缺省页 ```dart /// 配置全局缺省页 StateConfig.buildEmpty = (it) { return Text("empty"); }; StateConfig.buildError = (it, onTapRetry) { // 重试需要调用onTapRetry return Text("Error"); }; StateConfig.buildLoading = (it) { return Text("Loading"); }; /// 均可传入自定义对象,方便定制页面 stateCtrl.showLoading(); stateCtrl.showEmpty(); stateCtrl.showError(); stateCtrl.showContent(); ``` ## 初始化控制器 ```dart StateCtrl stateCtrl = StateCtrl( initState: const StateModel(StateEnum.content), // 初始状态,默认content onRefresh: () {} //刷新回调,调用showLoading/onRefresh时会触发 ); /// 均可传入自定义对象,方便自定义页面 stateCtrl.showLoading(); stateCtrl.showEmpty(); stateCtrl.showError(); stateCtrl.showContent(); stateCtrl.refresh() /// dispose stateCtrl.dispose() ``` ## StateWidget组件 ```dart StateWidget( ctrl: stateCtrl, buildLoading: null, buildEmpty: null, buildError: null, child: Center( child: GestureDetector(onTap: ctrl.testApi, child: Text("LaunchPage")), ), ); ``` # 下拉刷新 - [ ] [pull_to_refresh](https://pub.dev/packages/pull_to_refresh) 已停止维护 - [ ] [infinite_scroll_pagination](https://pub.dev/packages/infinite_scroll_pagination/example) 可实现预加载,但需要使用非常规组件,入侵性太强了放弃 - [x] [easy_refresh](https://pub.dev/packages/easy_refresh) 还在维护中,已结合缺省页封装 ## 全局配置 ```dart /// 刷新Header和Footer EasyRefresh.defaultHeaderBuilder = () => MaterialHeader(); EasyRefresh.defaultFooterBuilder = () => CupertinoFooter(emptyWidget: Text("没有更多了")); /// 默认起始页码 PageCtrl.defaultStartIndex = 1; ``` ## 初始化控制器 ```dart // 泛型是列表数据的类型 PageCtrl pageCtrl = PageCtrl( startIndex: 1, // 默认1 onRefresh: (pageCtrl, int index) async { // 模拟网络请求 await Future.delayed(Duration(seconds: 2)); pageCtrl.addData( data: ["$index-1", "$index-2", "$index-3", "$index-4", "$index-5"], hasMore: index < 3, //是否还有下一页 ); }, ); pageCtrl.showLoading(); //会回调 onRefresh 并重置页码 pageCtrl.showEmpty();//已在addData中处理 pageCtrl.showError(); pageCtrl.refresh(); //静默刷新 /// dispose pageCtrl.dispose() ``` ## PageWidget组件 ```dart PageWidget( pageCtrl: pageCtrl, child: (list) { return ListView.builder( itemCount: list.length, itemBuilder: (context, index) { return Container(height: 200, child: Text(list[index])); }, ); }, ), ``` # 一些扩展函数 ## Get.findOrNull **描述** 查找 GetX 容器中的控制器实例,若不存在返回 null **示例** ```dart Get.findOrNull()?.init(); ```