{"id":22945531,"url":"https://github.com/ducafecat/flutter_ducafecat_news_getx","last_synced_at":"2025-04-04T20:14:43.688Z","repository":{"id":37844357,"uuid":"372459697","full_name":"ducafecat/flutter_ducafecat_news_getx","owner":"ducafecat","description":"flutter2 + dio4 + getx4","archived":false,"fork":false,"pushed_at":"2023-05-04T15:14:25.000Z","size":4928,"stargazers_count":599,"open_issues_count":6,"forks_count":158,"subscribers_count":18,"default_branch":"master","last_synced_at":"2025-03-28T19:11:22.436Z","etag":null,"topics":["flutter2","getx","getxpattern","template","template-project"],"latest_commit_sha":null,"homepage":"","language":"Dart","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ducafecat.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2021-05-31T09:57:15.000Z","updated_at":"2025-03-25T05:49:53.000Z","dependencies_parsed_at":"2022-07-10T14:17:17.930Z","dependency_job_id":"656405e7-5e5e-49af-aa70-70d4c58b4916","html_url":"https://github.com/ducafecat/flutter_ducafecat_news_getx","commit_stats":null,"previous_names":[],"tags_count":0,"template":true,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ducafecat%2Fflutter_ducafecat_news_getx","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ducafecat%2Fflutter_ducafecat_news_getx/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ducafecat%2Fflutter_ducafecat_news_getx/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ducafecat%2Fflutter_ducafecat_news_getx/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ducafecat","download_url":"https://codeload.github.com/ducafecat/flutter_ducafecat_news_getx/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247242680,"owners_count":20907134,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["flutter2","getx","getxpattern","template","template-project"],"created_at":"2024-12-14T14:33:29.356Z","updated_at":"2025-04-04T20:14:43.663Z","avatar_url":"https://github.com/ducafecat.png","language":"Dart","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003ca href=\"https://ducafecat.com\" target=\"_blank\"\u003e\n  \u003cimg src=\"https://ducafecat.oss-cn-beijing.aliyuncs.com/ducafecat/video-ducafecat-banner.png\" alt=\"猫哥视频站\" \u003e\n\u003c/a\u003e\n\n# flutter_ducafecat_news_getx\n\n## 接口 api 说明\n\n- api 基础地址\n\nhttps://mock.apifox.cn/m1/1124717-0-default\n\n- 文档地址\n\nhttps://www.apifox.cn/apidoc/project-1124717/api-24266149\n\n- apifox 导入文件\n\n`doc/新闻客户端api.apifox.json`\n\nhttps://www.apifox.cn/\n\n## 说明\n\n新闻客户端 Getx 版本 - 项目模板\n\n\u003e 你以为看到了结果，其实一切只是刚刚开始！\n\n![](README/2021-07-24-14-47-42.png)\n\n## B 站视频\n\nhttps://space.bilibili.com/404904528/channel/detail?cid=177514\u0026ctype=0\n\n## 微信群 ducafecat\n\n## 前言\n\n我的这个代码主要不是为了完成业务，大家也看到了并没有很多业务。\n\n这个项目是一个模板，有的同学可能要问，模板干啥的么~\n\n## 如何提高代码质量+效率？\n\n![](README/architecture.png)\n\n### 1. 规范\n\n用习惯的方式去开发所有的项目，如：编码规范、目录规则、模型定义、布局方案。。。\n\n[Effective Dart: Style](https://dart.dev/guides/language/effective-dart/style)\n\n[Flutter_Go 代码开发规范.md](https://github.com/alibaba/flutter-go/blob/master/Flutter_Go%20%E4%BB%A3%E7%A0%81%E5%BC%80%E5%8F%91%E8%A7%84%E8%8C%83.md)\n\n### 2. 模板\n\n共性通用、常见的东西抽取出来，如：路由、全局数据、认证、鉴权、离线登录、接口管理、数据模型、程序升级、数据验证、三级缓存、错误收集、行为分析。。。\n\n### 3. 代码库\n\n这就是业务功能了，你可以都集中在一个单体的项目中（推荐），而不是很多包，不好管理。\n\n常见业务有：欢迎界面、注册、登录、三方登录、聊天、视频、拍照、SKU、购物车、分销、地图、消息推送、评论、瀑布流、分类订阅、属性表格、轮播。。。\n\n## 配套 vscode 插件\n\n- [GetX Snippets](https://marketplace.visualstudio.com/items?itemName=get-snippets.get-snippets)\n\n  必装 代码提示、代码块\n\n- [Json to Dart Model](https://marketplace.visualstudio.com/items?itemName=hirantha.json-to-dart)\n\n  支持空安全，推荐\n\n- [Flutter GetX Generator - 猫哥](https://marketplace.visualstudio.com/items?itemName=ducafecat.getx-template)\n\n  这个插件用来快速创建 `page` 代码，计划（json to dart、iconfont、test unit）\n\n## 参考\n\n- [get_cli](https://github.com/jonataslaw/get_cli)\n- [getx_pattern](https://kauemurakami.github.io/getx_pattern/)\n- [flutter-go](https://github.com/alibaba/flutter-go)\n- [猫哥新闻第一版 flutter_learn_news](https://github.com/ducafecat/flutter_learn_news)\n- [写夜子 flutter-getx-template](https://github.com/xieyezi/flutter-getx-template)\n- [猫哥 getx_quick_start](https://github.com/ducafecat/getx_quick_start)\n- [flutter_use](https://github.com/CNAD666/flutter_use)\n- [redux part-1-overview-concepts](https://redux.js.org/tutorials/essentials/part-1-overview-concepts)\n- [todo_getx](https://github.com/loicgeek/todo_getx)\n\n## Mock 数据\n\n- api\n\nhttps://yapi.ducafecat.tech/mock/11\n\n- 查看接口方式\n\n```\nhttps://yapi.ducafecat.tech\napi@ducafecat.tech\n123456\n```\n\n## 目录结构\n\n![](README/catalog.png)\n\n还是延续我第一版的目录结构，虽然 getx-cli 的目录也很简洁，但是我这个也没大问题。\n\n### common 通用组件\n\n| 名称        | 说明           |\n| ----------- | -------------- |\n| apis        | http 接口定义  |\n| entities    | 数据模型、实例 |\n| langs       | 多语言         |\n| middlewares | 中间件         |\n| routes      | 路由           |\n| services    | getx 全局      |\n| utils       | 工具           |\n| values      | 值             |\n| widgets     | 公共组件       |\n\n### pages 业务界面\n\n![One-way data flow](README/one-way-data-flow-04fe46332c1ccb3497ecb04b94e55b97.png)\n\n界面代码拆分也是继承了 redux 的设计思想，视图、动作、状态，进行拆分。\n\n| 名称            | 说明     |\n| --------------- | -------- |\n| bindings.dart   | 数据绑定 |\n| controller.dart | 控制器   |\n| index.dart      | 入口     |\n| state.dart      | 状态     |\n| view.dart       | 视图     |\n| widgets         | 组件     |\n\n## GetX 上下拉列表界面\n\n![](README/2021-07-24-13-54-17.png)\n\n### `RxList` 来处理 List 集合\n\nlib/pages/category/state.dart\n\n```dart\nclass CategoryState {\n  // 新闻翻页\n  RxList\u003cNewsItem\u003e newsList = \u003cNewsItem\u003e[].obs;\n}\n```\n\n### `StatefulWidget` 结合 `AutomaticKeepAliveClientMixin`\n\nlib/pages/category/widgets/news_page_list.dart\n\n```dart\nclass _NewsPageListState extends State\u003cNewsPageList\u003e\n    with AutomaticKeepAliveClientMixin {\n  @override\n  bool get wantKeepAlive =\u003e true;\n\n  final controller = Get.find\u003cCategoryController\u003e();\n\n```\n\n### `pull_to_refresh` 下拉组件\n\nlib/pages/category/widgets/news_page_list.dart\n\n```dart\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n    return GetX\u003cCategoryController\u003e(\n      init: controller,\n      builder: (controller) =\u003e SmartRefresher(\n        enablePullUp: true,\n        controller: controller.refreshController,\n        onRefresh: controller.onRefresh,\n        onLoading: controller.onLoading,\n        child: CustomScrollView(\n          slivers: [\n            SliverPadding(\n              padding: EdgeInsets.symmetric(\n                vertical: 0.w,\n                horizontal: 0.w,\n              ),\n              sliver: SliverList(\n                delegate: SliverChildBuilderDelegate(\n                  (content, index) {\n                    var item = controller.state.newsList[index];\n                    return newsListItem(item);\n                  },\n                  childCount: controller.state.newsList.length,\n                ),\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n```\n\n`controller: controller.refreshController` 上下拉控制器\n\n`onRefresh: controller.onRefresh` 下拉刷新数据\n\n`onLoading: controller.onLoading` 上拉载入数据\n\n`SliverChildBuilderDelegate` 动态构建每一项, `childCount` 告诉组件一共有多少数据\n\n### `controller` 中写入业务\n\nlib/pages/category/controller.dart\n\n- `onRefresh` 下拉刷新\n\n```dart\n  void onRefresh() {\n    fetchNewsList(isRefresh: true).then((_) {\n      refreshController.refreshCompleted(resetFooterState: true);\n    }).catchError((_) {\n      refreshController.refreshFailed();\n    });\n  }\n```\n\n`refreshController.refreshCompleted()` 刷新完成\n\n`refreshController.refreshFailed()` 刷新失败\n\n- `onLoading` 上拉载入\n\n```dart\n  void onLoading() {\n    if (state.newsList.length \u003c total) {\n      fetchNewsList().then((_) {\n        refreshController.loadComplete();\n      }).catchError((_) {\n        refreshController.loadFailed();\n      });\n    } else {\n      refreshController.loadNoData();\n    }\n  }\n```\n\n`refreshController.loadComplete()` 载入完成\n\n`refreshController.loadFailed()` 载入失败\n\n`refreshController.loadNoData()` 没有数据\n\n- `fetch` 所有数据\n\n```dart\n  // 拉取数据\n  Future\u003cvoid\u003e fetchNewsList({bool isRefresh = false}) async {\n    var result = await NewsAPI.newsPageList(\n      params: NewsPageListRequestEntity(\n        categoryCode: categoryCode,\n        pageNum: curPage + 1,\n        pageSize: pageSize,\n      ),\n    );\n\n    if (isRefresh == true) {\n      curPage = 1;\n      total = result.counts!;\n      state.newsList.clear();\n    } else {\n      curPage++;\n    }\n\n    state.newsList.addAll(result.items!);\n  }\n```\n\n`state.newsList.addAll(result.items!);` 合并 `list` 集合 `RxList` 封装的\n\n- `dispose` 记得释放\n\n```dart\n  ///dispose 释放内存\n  @override\n  void dispose() {\n    super.dispose();\n    // dispose 释放对象\n    refreshController.dispose();\n  }\n```\n\n`refreshController.dispose()` 这个业务中就是下拉控件了，还有视频播放器、文本框啥的控制器都要记得释放。\n\n- `bindings` 放在 `ApplicationBinding`\n\nlib/pages/application/bindings.dart\n\n```dart\nclass ApplicationBinding implements Bindings {\n  @override\n  void dependencies() {\n    Get.lazyPut\u003cApplicationController\u003e(() =\u003e ApplicationController());\n    Get.lazyPut\u003cMainController\u003e(() =\u003e MainController());\n    Get.lazyPut\u003cCategoryController\u003e(() =\u003e CategoryController());\n  }\n}\n```\n\n因为这个 `CategoryController` 是属于 `Application` 被路由载入的\n\n## 状态管理\n\n### `Bindings` 自动载入释放\n\n适合命名路由\n\n- 定义 `Bindings`\n\n```dart\nclass SignInBinding implements Bindings {\n  @override\n  void dependencies() {\n    Get.lazyPut\u003cSignInController\u003e(() =\u003e SignInController());\n  }\n}\n```\n\n- 路由定义\n\n```dart\nGetPage(\n  name: AppRoutes.SIGN_IN,\n  page: () =\u003e SignInPage(),\n  binding: SignInBinding(),\n),\n```\n\n- `Get.toNamed` 载入界面时自动管理响应数据\n\n```sh\nflutter: ** GOING TO ROUTE /home. isError: [false]\nflutter: ** GOING TO ROUTE /count. isError: [false]\nflutter: ** Instance \"CountController\" has been created. isError: [false]\nflutter: ** Instance \"CountController\" has been initialized. isError: [false]\nflutter: ** GOING TO ROUTE /count. isError: [false]\nflutter: ** CLOSE TO ROUTE /count. isError: [false]\nflutter: ** \"CountController\" onDelete() called. isError: [false]\nflutter: ** \"CountController\" deleted from memory. isError: [false]\n```\n\n### `Get.put` `Get.find` 手动管理\n\n适合非命名路由、组件实例化\n\n- `Get.put` 初始\n\n```dart\nclass StateDependencyPutFindView extends StatelessWidget {\n  StateDependencyPutFindView({Key? key}) : super(key: key);\n\n  final controller = Get.put\u003cCountController\u003e(CountController());\n```\n\n- `Get.find` 调用\n\n```dart\nclass NextPageView extends StatelessWidget {\n  NextPageView({Key? key}) : super(key: key);\n\n  final controller = Get.find\u003cCountController\u003e();\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(\"NextPage\"),\n      ),\n      body: Center(\n        child: Column(\n          children: [\n            GetX\u003cCountController\u003e(\n              init: controller,\n              initState: (_) {},\n              builder: (_) {\n                return Text('value -\u003e ${_.count}');\n              },\n            ),\n            Divider(),\n          ],\n        ),\n      ),\n    );\n  }\n}\n```\n\n## 组件设计\n\n### 直接使用 `GetView` 组件\n\n好处代码少，直接用 `controller` 成员变量访问\n\n```dart\nclass HellowordWidget extends GetView\u003cNotfoundController\u003e {\n  @override\n  Widget build(BuildContext context) {\n    return Center(\n      child: Obx(() =\u003e Text(controller.state.title)),\n    );\n  }\n}\n```\n\n### 遇到 `Mixin` 要自定义\n\n使用 `Mixin with` 特性，直接 `StatefulWidget` `StatelessWidget` 封装\n\n这是不可避免的\n\n- AutomaticKeepAliveClientMixin\n\n```dart\nclass _NewsPageListState extends State\u003cNewsPageList\u003e\n    with AutomaticKeepAliveClientMixin {\n  @override\n  bool get wantKeepAlive =\u003e true;\n\n  final controller = Get.find\u003cCategoryController\u003e();\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n```\n\n- TickerProviderStateMixin\n\n```dart\nclass StaggerRoute extends StatefulWidget {\n  @override\n  _StaggerRouteState createState() =\u003e _StaggerRouteState();\n}\n\nclass _StaggerRouteState extends State\u003cStaggerRoute\u003e with TickerProviderStateMixin {\n  final controller = Get.find\u003cStaggerController\u003e();\n```\n\n### 不要响应数据过度使用\n\n- 很多时候，你可能不需要响应数据\n\n  - 单页面数据列表\n  - 无夸页面、夸组件情况\n  - 表单处理\n\n- 推荐使用场景\n\n  - 全局数据: 用户信息、聊天推送、样式色彩主题\n  - 单页多组件交互：聊天界面\n  - 多页面切换：购物车\n\n\u003e 请分清楚 `GetX` 是一种组件的封装方式，他只是包含了 `路由`、`状态管理`、`弹出框` ...\n\n## Deep Linking 方式外部打开 APP\n\n- 效果\n\n![](README/scheme.gif)\n\n- 参考\n\n  - https://flutter.dev/docs/development/ui/navigation/deep-linking\n  - https://developer.android.com/codelabs/basic-android-kotlin-training-activities-intents#0\n  - https://developer.android.com/reference/android/content/Intent\n  - https://www.runoob.com/w3cnote/android-tutorial-intent-base.html\n  - https://pub.flutter-io.cn/packages/uni_links\n\n- android\n\n\u003e android/app/src/main/AndroidManifest.xml\n\n```xml\n\u003cactivity\n  ... \u003e\n  ...\n    \u003cintent-filter\u003e\n        \u003caction android:name=\"android.intent.action.VIEW\"/\u003e\n        \u003ccategory android:name=\"android.intent.category.DEFAULT\"/\u003e\n        \u003ccategory android:name=\"android.intent.category.BROWSABLE\"/\u003e\n        \u003cdata\n            android:scheme=\"newsgetx\"\n            /\u003e\n    \u003c/intent-filter\u003e\n\u003c/activity\u003e\n```\n\n- ios\n\n\u003e Runner -\u003e TARGETS -\u003e Info -\u003e URL Types\n\n![](README/2021-09-18-22-26-23.png)\n\n- 插件 uni_links\n\n```yaml\ndependencies:\n  ...\n  uni_links: ^0.5.1\n```\n\n- flutter 代码\n\n\u003e lib/pages/application/controller.dart\n\n```dart\n\n  /// scheme 内部打开\n  bool isInitialUriIsHandled = false;\n  StreamSubscription? uriSub;\n\n  // 第一次打开\n  Future\u003cvoid\u003e handleInitialUri() async {\n    if (!isInitialUriIsHandled) {\n      isInitialUriIsHandled = true;\n      try {\n        final uri = await getInitialUri();\n        if (uri == null) {\n          print('no initial uri');\n        } else {\n          // 这里获取了 scheme 请求\n          print('got initial uri: $uri');\n        }\n      } on PlatformException {\n        print('falied to get initial uri');\n      } on FormatException catch (err) {\n        print('malformed initial uri, ' + err.toString());\n      }\n    }\n  }\n\n  // 程序打开时介入\n  void handleIncomingLinks() {\n    if (!kIsWeb) {\n      uriSub = uriLinkStream.listen((Uri? uri) {\n        // 这里获取了 scheme 请求\n        print('got uri: $uri');\n\n        if (uri != null \u0026\u0026 uri.path == '/notify/category') {\n          Get.toNamed(AppRoutes.Category);\n        }\n      }, onError: (Object err) {\n        print('got err: $err');\n      });\n    }\n  }\n\n  @override\n  void dispose() {\n    uriSub?.cancel();\n    super.dispose();\n  }\n```\n\n- 网页中调用\n\n```html\n\u003ca href=\"newsgetx://com.tpns.push/notify/category\"\n  \u003enewsgetx://com.tpns.push/notify/category\u003c/a\n\u003e\n\n\u003ca href=\"newsgetx://com.tpns.push/notify/message/123\"\n  \u003enewsgetx://com.tpns.push/notify/message/123\u003c/a\n\u003e\n```\n\n- 结果\n\n![](README/2021-09-18-22-52-54.png)\n\n## 路由设计\n\n通过 GetPage 方式声明 名称、组件、数据绑定、中间件\n\n文件\n\n`lib/common/routes/pages.dart`\n\n```dart\nclass AppRoutes {\n  static const INITIAL = '/';\n  static const SIGN_IN = '/sign_in';\n  static const SIGN_UP = '/sign_up';\n  static const NotFound = '/not_found';\n\n  static const Application = '/application';\n  static const Category = '/category';\n}\n```\n\n`lib/common/routes/names.dart`\n\n```dart\nclass AppPages {\n  static const INITIAL = AppRoutes.INITIAL;\n  static final RouteObserver\u003cRoute\u003e observer = RouteObservers();\n  static List\u003cString\u003e history = [];\n\n  static final List\u003cGetPage\u003e routes = [\n    // 免登陆\n    GetPage(\n      name: AppRoutes.INITIAL,\n      page: () =\u003e WelcomePage(),\n      binding: WelcomeBinding(),\n      middlewares: [\n        RouteWelcomeMiddleware(priority: 1),\n      ],\n    ),\n    ...\n```\n\n## 中间件\n\n### 登录验证\n\n通过 继承 `GetMiddleware` 并重写 `redirect` 方法，如果没有登录，指向登录页。\n\n`lib/common/middlewares/router_auth.dart`\n\n```dart\n/// 检查是否登录\nclass RouteAuthMiddleware extends GetMiddleware {\n  // priority 数字小优先级高\n  @override\n  int? priority = 0;\n\n  RouteAuthMiddleware({required this.priority});\n\n  @override\n  RouteSettings? redirect(String? route) {\n    if (UserStore.to.isLogin ||\n        route == AppRoutes.SIGN_IN ||\n        route == AppRoutes.SIGN_UP ||\n        route == AppRoutes.INITIAL) {\n      return null;\n    } else {\n      Future.delayed(\n          Duration(seconds: 1), () =\u003e Get.snackbar(\"提示\", \"登录过期,请重新登录\"));\n      return RouteSettings(name: AppRoutes.SIGN_IN);\n    }\n  }\n}\n```\n\n### 欢迎屏幕\n\n如果是第一次登录去欢迎屏幕，已登录的去首页，没登录的去登录页。\n\n`lib/common/middlewares/router_welcome.dart`\n\n```dart\n/// 第一次欢迎页面\nclass RouteWelcomeMiddleware extends GetMiddleware {\n  // priority 数字小优先级高\n  @override\n  int? priority = 0;\n\n  RouteWelcomeMiddleware({required this.priority});\n\n  @override\n  RouteSettings? redirect(String? route) {\n    if (ConfigStore.to.isFirstOpen == true) {\n      return null;\n    } else if (UserStore.to.isLogin == true) {\n      return RouteSettings(name: AppRoutes.Application);\n    } else {\n      return RouteSettings(name: AppRoutes.SIGN_IN);\n    }\n  }\n}\n```\n\n## 全局数据\n\n主要是采用 `GetxService` 的全局机制，把一些需要初始化 全局使用的功能封装起来，如这里的本地持久化。\n\n`lib/common/services/storage.dart`\n\n```dart\nclass StorageService extends GetxService {\n  static StorageService get to =\u003e Get.find();\n  late final SharedPreferences _prefs;\n\n  Future\u003cStorageService\u003e init() async {\n    _prefs = await SharedPreferences.getInstance();\n    return this;\n  }\n```\n\n\u003e 注意这里的 单例方式 `static StorageService get to =\u003e Get.find();`\n\u003e\n\u003e 以后全局使用可以 `StorageService.to.xxx`\n\n定义完之后，在 `run man` 之前完成必要的初始，有些其实可以懒加载，这样不卡 `io`。\n\n`lib/global.dart`\n\n```dart\nclass Global {\n  /// 初始化\n  static Future init() async {\n    WidgetsFlutterBinding.ensureInitialized();\n    await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);\n\n    setSystemUi();\n    Loading();\n\n    await Get.putAsync\u003cStorageService\u003e(() =\u003e StorageService().init());\n\n    Get.put\u003cConfigStore\u003e(ConfigStore());\n    Get.put\u003cUserStore\u003e(UserStore());\n  }\n\n  static void setSystemUi() {\n    if (GetPlatform.isAndroid) {\n      SystemUiOverlayStyle systemUiOverlayStyle = SystemUiOverlayStyle(\n        statusBarColor: Colors.transparent,\n        statusBarBrightness: Brightness.light,\n        statusBarIconBrightness: Brightness.dark,\n        systemNavigationBarDividerColor: Colors.transparent,\n        systemNavigationBarColor: Colors.white,\n        systemNavigationBarIconBrightness: Brightness.dark,\n      );\n      SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle);\n    }\n  }\n}\n\n```\n\n\u003e 这里的 `init` 方法就是我们要优先 `runApp` 执行的方法\n\u003e\n\u003e 如果要异步初始，这样调用 `await Get.putAsync\u003cStorageService\u003e(() =\u003e StorageService().init());`\n\u003e\n\u003e 通过 `Get.put\u003cConfigStore\u003e(ConfigStore());` 这样的方式初始全局对象\n\n`lib/main.dart`\n\n```dart\nFuture\u003cvoid\u003e main() async {\n  await Global.init();\n  runApp(MyApp());\n}\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return ScreenUtilInit(\n      designSize: Size(375, 812),\n      builder: () =\u003e RefreshConfiguration(\n        headerBuilder: () =\u003e ClassicHeader(),\n        footerBuilder: () =\u003e ClassicFooter(),\n        hideFooterWhenNotFull: true,\n        headerTriggerDistance: 80,\n        maxOverScrollExtent: 100,\n        footerTriggerDistance: 150,\n        child: GetMaterialApp(\n          title: 'News',\n          theme: AppTheme.light,\n          debugShowCheckedModeBanner: false,\n          initialRoute: AppPages.INITIAL,\n          getPages: AppPages.routes,\n          builder: EasyLoading.init(),\n          translations: TranslationService(),\n          navigatorObservers: [AppPages.observer],\n          localizationsDelegates: [\n            GlobalMaterialLocalizations.delegate,\n            GlobalWidgetsLocalizations.delegate,\n            GlobalCupertinoLocalizations.delegate,\n          ],\n          supportedLocales: ConfigStore.to.languages,\n          locale: ConfigStore.to.locale,\n          fallbackLocale: Locale('en', 'US'),\n          enableLog: true,\n          logWriterCallback: Logger.write,\n        ),\n      ),\n    );\n  }\n}\n```\n\n\u003e 我写了个 `main() async` 按顺序同步执行\n\u003e\n\u003e 这个 `MyApp` 比较典型，包含了 `ScreenUtilInit` `RefreshConfiguration` `GetMaterialApp` `EasyLoading` `translations` `getPages` `theme` 这些初始，大家可以参考\n\n## 本地数据持久化\n\n用到了组件 `shared_preferences`\n\n封装成了全局对象 `lib/common/services/storage.dart`\n\n```dart\nclass StorageService extends GetxService {\n  static StorageService get to =\u003e Get.find();\n  late final SharedPreferences _prefs;\n\n  Future\u003cStorageService\u003e init() async {\n    _prefs = await SharedPreferences.getInstance();\n    return this;\n  }\n\n  Future\u003cbool\u003e setString(String key, String value) async {\n    return await _prefs.setString(key, value);\n  }\n\n  Future\u003cbool\u003e setBool(String key, bool value) async {\n    return await _prefs.setBool(key, value);\n  }\n\n  Future\u003cbool\u003e setList(String key, List\u003cString\u003e value) async {\n    return await _prefs.setStringList(key, value);\n  }\n\n  String getString(String key) {\n    return _prefs.getString(key) ?? '';\n  }\n\n  bool getBool(String key) {\n    return _prefs.getBool(key) ?? false;\n  }\n\n  List\u003cString\u003e getList(String key) {\n    return _prefs.getStringList(key) ?? [];\n  }\n\n  Future\u003cbool\u003e remove(String key) async {\n    return await _prefs.remove(key);\n  }\n}\n\n```\n\n\u003e 单例方式访问 `StorageService.to.setString(xxxx)`\n\n## 数据模型\n\n推荐大家使用三方的 json to model 插件\n\n我这边用的是 [Paste JSON as Code](https://marketplace.visualstudio.com/items?itemName=quicktype.quicktype)\n\n这些实例对象都放在了 `lib/common/entities` 目录下\n\n有一点要建议大家，就是在 api 接口请求的时候 也要写实例对象来严格控制类型，方便排错，否则都是 `map` 后期大家都不好维护。\n\n举例 `lib/common/entities/user.dart`\n\n```dart\n// 注册请求\nclass UserRegisterRequestEntity {\n  String email;\n  String password;\n\n  UserRegisterRequestEntity({\n    required this.email,\n    required this.password,\n  });\n\n  factory UserRegisterRequestEntity.fromJson(Map\u003cString, dynamic\u003e json) =\u003e\n      UserRegisterRequestEntity(\n        email: json[\"email\"],\n        password: json[\"password\"],\n      );\n\n  Map\u003cString, dynamic\u003e toJson() =\u003e {\n        \"email\": email,\n        \"password\": password,\n      };\n}\n\n// 登录请求\nclass UserLoginRequestEntity {\n  String email;\n  String password;\n\n  UserLoginRequestEntity({\n    required this.email,\n    required this.password,\n  });\n\n  factory UserLoginRequestEntity.fromJson(Map\u003cString, dynamic\u003e json) =\u003e\n      UserLoginRequestEntity(\n        email: json[\"email\"],\n        password: json[\"password\"],\n      );\n\n  Map\u003cString, dynamic\u003e toJson() =\u003e {\n        \"email\": email,\n        \"password\": password,\n      };\n}\n\n// 登录返回\nclass UserLoginResponseEntity {\n  String? accessToken;\n  String? displayName;\n  List\u003cString\u003e? channels;\n\n  UserLoginResponseEntity({\n    this.accessToken,\n    this.displayName,\n    this.channels,\n  });\n\n  factory UserLoginResponseEntity.fromJson(Map\u003cString, dynamic\u003e json) =\u003e\n      UserLoginResponseEntity(\n        accessToken: json[\"access_token\"],\n        displayName: json[\"display_name\"],\n        channels: List\u003cString\u003e.from(json[\"channels\"].map((x) =\u003e x)),\n      );\n\n  Map\u003cString, dynamic\u003e toJson() =\u003e {\n        \"access_token\": accessToken,\n        \"display_name\": displayName,\n        \"channels\":\n            channels == null ? [] : List\u003cdynamic\u003e.from(channels!.map((x) =\u003e x)),\n      };\n}\n\n```\n\n\u003e 可以看到 `UserRegisterRequestEntity` 就是请求的时候的对象\n\napi 接口代码\n\n```dart\n/// 用户\nclass UserAPI {\n  /// 登录\n  static Future\u003cUserLoginResponseEntity\u003e login({\n    UserLoginRequestEntity? params,\n  }) async {\n    var response = await HttpUtil().post(\n      '/user/login',\n      data: params?.toJson(),\n    );\n    return UserLoginResponseEntity.fromJson(response);\n  }\n```\n\n\u003e 可以看到这个接口的输入输出都已经包装好，这样强类型 后期排错 很方便。\n\n## http 拉取数据\n\n我并没有用 `GetConnect` ，而是采用了 `dio` ，主要还是考虑稳健性。\n\n所有的操作还是封装在了 `lib/common/utils/http.dart`\n\n\u003e 代码我就补贴了，篇幅太长，大家自己看下\n\u003e\n\u003e 封装了常用的 restful 操作 `get` `post` `put` `delete` `patch`\n\u003e\n\u003e 为了适合个别服务端组件又加入 `postForm` `postStream`\n\u003e\n\u003e 错误处理 `onError` 罗列了常见的错误\n\n## 用户登录注销\u0026401\n\n注销的时候需要清理下本地的缓存，比如 `token` `profile` 这类数据。\n\n具体代码可以参考 `lib/common/store/user.dart`\n\n```dart\n  // 注销\n  Future\u003cvoid\u003e onLogout() async {\n    if (_isLogin.value) await UserAPI.logout();\n    await StorageService.to.remove(STORAGE_USER_TOKEN_KEY);\n    _isLogin.value = false;\n    token = '';\n  }\n```\n\n再来说说 401 ，这是服务器返回的没有授权的状态，我们获取后需要弹出登录界面。\n\n这个操作可以放在 dio 的错误处理 `lib/common/utils/http.dart`\n\n```dart\n// 错误处理\nvoid onError(ErrorEntity eInfo) {\n    print('error.code -\u003e ' +\n        eInfo.code.toString() +\n        ', error.message -\u003e ' +\n        eInfo.message);\n    switch (eInfo.code) {\n      case 401:\n        UserStore.to.onLogout();\n        EasyLoading.showError(eInfo.message);\n        break;\n      default:\n        EasyLoading.showError('未知错误');\n        break;\n    }\n  }\n```\n\n\u003e 一旦发现 `eInfo.code` 是 `401` ，就直接 onLogout 操作，并弹出消息提示。\n\n`ErrorEntity` 是我封装的错误信息格式化\n\n```dart\n// 错误信息\n  ErrorEntity createErrorEntity(DioError error) {\n    switch (error.type) {\n      case DioErrorType.cancel:\n        return ErrorEntity(code: -1, message: \"请求取消\");\n      case DioErrorType.connectTimeout:\n        return ErrorEntity(code: -1, message: \"连接超时\");\n      case DioErrorType.sendTimeout:\n        return ErrorEntity(code: -1, message: \"请求超时\");\n      case DioErrorType.receiveTimeout:\n        return ErrorEntity(code: -1, message: \"响应超时\");\n      case DioErrorType.cancel:\n        return ErrorEntity(code: -1, message: \"请求取消\");\n      case DioErrorType.response:\n        {\n          try {\n            int errCode =\n                error.response != null ? error.response!.statusCode! : -1;\n            // String errMsg = error.response.statusMessage;\n            // return ErrorEntity(code: errCode, message: errMsg);\n            switch (errCode) {\n              case 400:\n                return ErrorEntity(code: errCode, message: \"请求语法错误\");\n              case 401:\n                return ErrorEntity(code: errCode, message: \"没有权限\");\n              case 403:\n                return ErrorEntity(code: errCode, message: \"服务器拒绝执行\");\n              case 404:\n                return ErrorEntity(code: errCode, message: \"无法连接服务器\");\n              case 405:\n                return ErrorEntity(code: errCode, message: \"请求方法被禁止\");\n              case 500:\n                return ErrorEntity(code: errCode, message: \"服务器内部错误\");\n              case 502:\n                return ErrorEntity(code: errCode, message: \"无效的请求\");\n              case 503:\n                return ErrorEntity(code: errCode, message: \"服务器挂了\");\n              case 505:\n                return ErrorEntity(code: errCode, message: \"不支持HTTP协议请求\");\n              default:\n                {\n                  // return ErrorEntity(code: errCode, message: \"未知错误\");\n                  return ErrorEntity(\n                    code: errCode,\n                    message: error.response != null\n                        ? error.response!.statusMessage!\n                        : \"\",\n                  );\n                }\n            }\n          } on Exception catch (_) {\n            return ErrorEntity(code: -1, message: \"未知错误\");\n          }\n        }\n      default:\n        {\n          return ErrorEntity(code: -1, message: error.message);\n        }\n    }\n  }\n```\n\n## 动态权限\n\n前端这边如果涉及权限的检查，你还是可以写到路由中间件中，一旦发现路由变动就去鉴权，看看是否有权限，这个用户的权限可以再拉取 `profile` 中的 `rules` 这样的信息。\n\n```dart\nclass AuthorityMiddleware extends GetMiddleware {\n  // priority 数字小优先级高\n  @override\n  int? priority = 0;\n\n  AuthorityMiddleware({required this.priority});\n\n  @override\n  RouteSettings? redirect(String? route) {\n    ......\n    在这里实现\n  }\n}\n```\n\n## 用户数据\n\n这个需要全局化 `lib/common/store/user.dart`\n\n把用户的 `token` `profile` 是否登录，这样的状态都维护起来。\n\n```dart\nclass UserStore extends GetxController {\n  static UserStore get to =\u003e Get.find();\n\n  // 是否登录\n  final _isLogin = false.obs;\n  // 令牌 token\n  String token = '';\n  // 用户 profile\n  final _profile = UserLoginResponseEntity().obs;\n\n  bool get isLogin =\u003e _isLogin.value;\n  UserLoginResponseEntity get profile =\u003e _profile.value;\n  bool get hasToken =\u003e token.isNotEmpty;\n\n  @override\n  void onInit() {\n    super.onInit();\n    token = StorageService.to.getString(STORAGE_USER_TOKEN_KEY);\n    var profileOffline = StorageService.to.getString(STORAGE_USER_PROFILE_KEY);\n    if (profileOffline.isNotEmpty) {\n      _profile(UserLoginResponseEntity.fromJson(jsonDecode(profileOffline)));\n    }\n  }\n\n  // 保存 token\n  Future\u003cvoid\u003e setToken(String value) async {\n    await StorageService.to.setString(STORAGE_USER_TOKEN_KEY, value);\n    token = value;\n  }\n\n  // 获取 profile\n  Future\u003cvoid\u003e getProfile() async {\n    if (token.isEmpty) return;\n    var result = await UserAPI.profile();\n    _profile(result);\n    _isLogin.value = true;\n    StorageService.to.setString(STORAGE_USER_PROFILE_KEY, jsonEncode(result));\n  }\n\n  // 保存 profile\n  Future\u003cvoid\u003e saveProfile(UserLoginResponseEntity profile) async {\n    _isLogin.value = true;\n    StorageService.to.setString(STORAGE_USER_PROFILE_KEY, jsonEncode(profile));\n  }\n\n  // 注销\n  Future\u003cvoid\u003e onLogout() async {\n    if (_isLogin.value) await UserAPI.logout();\n    await StorageService.to.remove(STORAGE_USER_TOKEN_KEY);\n    _isLogin.value = false;\n    token = '';\n  }\n}\n\n```\n\n## 一些常见错误\n\n### macos 下 network 没有权限\n\n错误信息\n\n```\nSocketException: Connection failed (OS Error: Operation not permitted, errno = 1)\n```\n\n解决\n\n`macos/Runner/DebugProfile.entitlements`\n\n```\n\u003ckey\u003ecom.apple.security.network.client\u003c/key\u003e\n\u003ctrue/\u003e\n```\n\nend\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fducafecat%2Fflutter_ducafecat_news_getx","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fducafecat%2Fflutter_ducafecat_news_getx","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fducafecat%2Fflutter_ducafecat_news_getx/lists"}