flutter2 + dio4 + getx4

Overview

flutter_ducafecat_news_getx

新闻客户端 Getx 版本 - 项目模板

你以为看到了结果,其实一切只是刚刚开始!

B 站视频

https://space.bilibili.com/404904528/channel/detail?cid=177514&ctype=0

微信群 ducafecat

前言

我的这个代码主要不是为了完成业务,大家也看到了并没有很多业务。

这个项目是一个模板,有的同学可能要问,模板干啥的么~

如何提高代码质量+效率?

1. 规范

用习惯的方式去开发所有的项目,如:编码规范、目录规则、模型定义、布局方案。。。

Effective Dart: Style

Flutter_Go 代码开发规范.md

2. 模板

共性通用、常见的东西抽取出来,如:路由、全局数据、认证、鉴权、离线登录、接口管理、数据模型、程序升级、数据验证、三级缓存、错误收集、行为分析。。。

3. 代码库

这就是业务功能了,你可以都集中在一个单体的项目中(推荐),而不是很多包,不好管理。

常见业务有:欢迎界面、注册、登录、三方登录、聊天、视频、拍照、SKU、购物车、分销、地图、消息推送、评论、瀑布流、分类订阅、属性表格、轮播。。。

配套 vscode 插件

参考

目录结构

还是延续我第一版的目录结构,虽然 getx-cli 的目录也很简洁,但是我这个也没大问题。

common 通用组件

名称 说明
apis http 接口定义
entities 数据模型、实例
langs 多语言
middlewares 中间件
routes 路由
services getx 全局
utils 工具
values
widgets 公共组件

pages 业务界面

One-way data flow

界面代码拆分也是继承了 redux 的设计思想,视图、动作、状态,进行拆分。

名称 说明
bindings.dart 数据绑定
controller.dart 控制器
index.dart 入口
state.dart 状态
view.dart 视图
widgets 组件

GetX 上下拉列表界面

RxList 来处理 List 集合

lib/pages/category/state.dart

class CategoryState {
  // 新闻翻页
  RxList<NewsItem> newsList = <NewsItem>[].obs;
}

StatefulWidget 结合 AutomaticKeepAliveClientMixin

lib/pages/category/widgets/news_page_list.dart

class _NewsPageListState extends State<NewsPageList>
    with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;

  final controller = Get.find<CategoryController>();

pull_to_refresh 下拉组件

lib/pages/category/widgets/news_page_list.dart

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return GetX<CategoryController>(
      init: controller,
      builder: (controller) => SmartRefresher(
        enablePullUp: true,
        controller: controller.refreshController,
        onRefresh: controller.onRefresh,
        onLoading: controller.onLoading,
        child: CustomScrollView(
          slivers: [
            SliverPadding(
              padding: EdgeInsets.symmetric(
                vertical: 0.w,
                horizontal: 0.w,
              ),
              sliver: SliverList(
                delegate: SliverChildBuilderDelegate(
                  (content, index) {
                    var item = controller.state.newsList[index];
                    return newsListItem(item);
                  },
                  childCount: controller.state.newsList.length,
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

controller: controller.refreshController 上下拉控制器

onRefresh: controller.onRefresh 下拉刷新数据

onLoading: controller.onLoading 上拉载入数据

SliverChildBuilderDelegate 动态构建每一项, childCount 告诉组件一共有多少数据

controller 中写入业务

lib/pages/category/controller.dart

  • onRefresh 下拉刷新
  void onRefresh() {
    fetchNewsList(isRefresh: true).then((_) {
      refreshController.refreshCompleted(resetFooterState: true);
    }).catchError((_) {
      refreshController.refreshFailed();
    });
  }

refreshController.refreshCompleted() 刷新完成

refreshController.refreshFailed() 刷新失败

  • onLoading 上拉载入
  void onLoading() {
    if (state.newsList.length < total) {
      fetchNewsList().then((_) {
        refreshController.loadComplete();
      }).catchError((_) {
        refreshController.loadFailed();
      });
    } else {
      refreshController.loadNoData();
    }
  }

refreshController.loadComplete() 载入完成

refreshController.loadFailed() 载入失败

refreshController.loadNoData() 没有数据

  • fetch 所有数据
  // 拉取数据
  Future<void> fetchNewsList({bool isRefresh = false}) async {
    var result = await NewsAPI.newsPageList(
      params: NewsPageListRequestEntity(
        categoryCode: categoryCode,
        pageNum: curPage + 1,
        pageSize: pageSize,
      ),
    );

    if (isRefresh == true) {
      curPage = 1;
      total = result.counts!;
      state.newsList.clear();
    } else {
      curPage++;
    }

    state.newsList.addAll(result.items!);
  }

state.newsList.addAll(result.items!); 合并 list 集合 RxList 封装的

  • dispose 记得释放
  ///dispose 释放内存
  @override
  void dispose() {
    super.dispose();
    // dispose 释放对象
    refreshController.dispose();
  }

refreshController.dispose() 这个业务中就是下拉控件了,还有视频播放器、文本框啥的控制器都要记得释放。

  • bindings 放在 ApplicationBinding

lib/pages/application/bindings.dart

class ApplicationBinding implements Bindings {
  @override
  void dependencies() {
    Get.lazyPut<ApplicationController>(() => ApplicationController());
    Get.lazyPut<MainController>(() => MainController());
    Get.lazyPut<CategoryController>(() => CategoryController());
  }
}

因为这个 CategoryController 是属于 Application 被路由载入的

状态管理

Bindings 自动载入释放

适合命名路由

  • 定义 Bindings
class SignInBinding implements Bindings {
  @override
  void dependencies() {
    Get.lazyPut<SignInController>(() => SignInController());
  }
}
  • 路由定义
GetPage(
  name: AppRoutes.SIGN_IN,
  page: () => SignInPage(),
  binding: SignInBinding(),
),
  • Get.toNamed 载入界面时自动管理响应数据
flutter: ** GOING TO ROUTE /home. isError: [false]
flutter: ** GOING TO ROUTE /count. isError: [false]
flutter: ** Instance "CountController" has been created. isError: [false]
flutter: ** Instance "CountController" has been initialized. isError: [false]
flutter: ** GOING TO ROUTE /count. isError: [false]
flutter: ** CLOSE TO ROUTE /count. isError: [false]
flutter: ** "CountController" onDelete() called. isError: [false]
flutter: ** "CountController" deleted from memory. isError: [false]

Get.put Get.find 手动管理

适合非命名路由、组件实例化

  • Get.put 初始
class StateDependencyPutFindView extends StatelessWidget {
  StateDependencyPutFindView({Key? key}) : super(key: key);

  final controller = Get.put<CountController>(CountController());
  • Get.find 调用
class NextPageView extends StatelessWidget {
  NextPageView({Key? key}) : super(key: key);

  final controller = Get.find<CountController>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("NextPage"),
      ),
      body: Center(
        child: Column(
          children: [
            GetX<CountController>(
              init: controller,
              initState: (_) {},
              builder: (_) {
                return Text('value -> ${_.count}');
              },
            ),
            Divider(),
          ],
        ),
      ),
    );
  }
}

组件设计

直接使用 GetView 组件

好处代码少,直接用 controller 成员变量访问

class HellowordWidget extends GetView<NotfoundController> {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Obx(() => Text(controller.state.title)),
    );
  }
}

遇到 Mixin 要自定义

使用 Mixin with 特性,直接 StatefulWidget StatelessWidget 封装

这是不可避免的

  • AutomaticKeepAliveClientMixin
class _NewsPageListState extends State<NewsPageList>
    with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;

  final controller = Get.find<CategoryController>();

  @override
  Widget build(BuildContext context) {
    super.build(context);
  • TickerProviderStateMixin
class StaggerRoute extends StatefulWidget {
  @override
  _StaggerRouteState createState() => _StaggerRouteState();
}

class _StaggerRouteState extends State<StaggerRoute> with TickerProviderStateMixin {
  final controller = Get.find<StaggerController>();

不要响应数据过度使用

  • 很多时候,你可能不需要响应数据

    • 单页面数据列表
    • 无夸页面、夸组件情况
    • 表单处理
  • 推荐使用场景

    • 全局数据: 用户信息、聊天推送、样式色彩主题
    • 单页多组件交互:聊天界面
    • 多页面切换:购物车

请分清楚 GetX 是一种组件的封装方式,他只是包含了 路由状态管理弹出框 ...

路由设计

全局数据

数据模型

http 拉取数据

用户登录注销&401

动态权限

APP 升级

sentry 错误收集

iconfont 矢量图标

test 单元测试

埋点

数据缓存

样式全局配置

国际化

GRAPHQL

数据加密安全

编译发布

CICD

to be continued ...

You might also like...
Comments
  • 可以在我的开源项目里面直接copy你的代码吗?

    可以在我的开源项目里面直接copy你的代码吗?

    我的项目使用了 木兰宽松许可证第二版(MulanPSL v2),并且我在你的代码里面没有看到任何”开源协议“

    所以发一个 Issue ,向你请求”许可我’copy‘你的代码”?(如果你不同意,我会再收到你的确认后删除之)

    你的文件 https://github.com/ducafecat/flutter_ducafecat_news_getx/blob/master/lib/common/services/storage.dart

    我之后copy 过来了,https://gitee.com/imboy-pub/imboy-flutter/blob/dev/lib/service/storage.dart

    opened by leeyisoft 3
  • BLANK screen on Desktop(Win10) and Web(Chrome)

    BLANK screen on Desktop(Win10) and Web(Chrome)

    when I ran :

    flutter create . flutter run -d chrome

    I received the Error:

    starting services ... Error: Unsupported operation: Platform._operatingSystem at Object.throw_ [as throw] (http://localhost:60292/dart_sdk.js:5041:11) at Function._operatingSystem (http://localhost:60292/dart_sdk.js:53433:17) at Function.get operatingSystem [as operatingSystem] (http://localhost:60292/dart_sdk.js:53479:27) at get _operatingSystem (http://localhost:60292/dart_sdk.js:53392:27) at Function.desc.get [as _operatingSystem] (http://localhost:60292/dart_sdk.js:5530:17) at get isIOS (http://localhost:60292/dart_sdk.js:53416:26) at Function.desc.get [as isIOS] (http://localhost:60292/dart_sdk.js:5530:17) at global_config.GlobalConfigService.new.init (http://localhost:60292/packages/flutter_ducafecat_news_getx/common/services/global_config.dart.lib.js:34:4 0) at init.next () at runBody (http://localhost:60292/dart_sdk.js:37422:34) at Object._async [as async] (http://localhost:60292/dart_sdk.js:37453:7) at global_config.GlobalConfigService.new.init (http://localhost:60292/packages/flutter_ducafecat_news_getx/common/services/global_config.dart.lib.js:32:2 0) at http://localhost:60292/packages/flutter_ducafecat_news_getx/main.dart.lib.js:178:156 at get_instance.GetInstance..putAsync (http://localhost:60292/packages/get/get_instance/src/get_instance.dart.lib.js:138:34) at putAsync.next () at runBody (http://localhost:60292/dart_sdk.js:37422:34) at Object._async [as async] (http://localhost:60292/dart_sdk.js:37453:7) at get_instance.GetInstance..putAsync (http://localhost:60292/packages/get/get_instance/src/get_instance.dart.lib.js:137:20) at Inst$124putAsync (http://localhost:60292/packages/get/get_instance/src/extension_instance.dart.lib.js:79:45) at Inst$124putAsync.next () at runBody (http://localhost:60292/dart_sdk.js:37422:34) at Object._async [as async] (http://localhost:60292/dart_sdk.js:37453:7) at Object.Inst$124putAsync [as Inst|putAsync] (http://localhost:60292/packages/get/get_instance/src/extension_instance.dart.lib.js:78:18) at initServices (http://localhost:60292/packages/flutter_ducafecat_news_getx/main.dart.lib.js:178:48)
    at initServices.next () at runBody (http://localhost:60292/dart_sdk.js:37422:34) at Object._async [as async] (http://localhost:60292/dart_sdk.js:37453:7) at Object.initServices (http://localhost:60292/packages/flutter_ducafecat_news_getx/main.dart.lib.js:176:18) at main$ (http://localhost:60292/packages/flutter_ducafecat_news_getx/main.dart.lib.js:171:18) at main$.next () at runBody (http://localhost:60292/dart_sdk.js:37422:34) at Object._async [as async] (http://localhost:60292/dart_sdk.js:37453:7) at main$ (http://localhost:60292/packages/flutter_ducafecat_news_getx/main.dart.lib.js:170:18) at main (http://localhost:60292/web_entrypoint.dart.lib.js:37:29) at main.next () at http://localhost:60292/dart_sdk.js:37403:33 at _RootZone.runUnary (http://localhost:60292/dart_sdk.js:37274:59) at _FutureListener.thenAwait.handleValue (http://localhost:60292/dart_sdk.js:32530:29) at handleValueCallback (http://localhost:60292/dart_sdk.js:33057:49) at Function._propagateToListeners (http://localhost:60292/dart_sdk.js:33095:17) at _Future.new.[_completeWithValue] (http://localhost:60292/dart_sdk.js:32943:23) at http://localhost:60292/dart_sdk.js:32194:46 at _RootZone.runUnary (http://localhost:60292/dart_sdk.js:37274:59) at _FutureListener.then.handleValue (http://localhost:60292/dart_sdk.js:32530:29) at handleValueCallback (http://localhost:60292/dart_sdk.js:33057:49) at Function._propagateToListeners (http://localhost:60292/dart_sdk.js:33095:17) at _Future.new.[_completeWithValue] (http://localhost:60292/dart_sdk.js:32943:23) at async._AsyncCallbackEntry.new.callback (http://localhost:60292/dart_sdk.js:32964:35) at Object._microtaskLoop (http://localhost:60292/dart_sdk.js:37526:13) at _startMicrotaskLoop (http://localhost:60292/dart_sdk.js:37532:13) at http://localhost:60292/dart_sdk.js:33303:9

    It is the the easist way to debug a app on Desktop and Web devices. I guess maybe the project is not ready for Desktop and Web.

    opened by LWY6670 2
  • 添加两个参数就不报错了

    添加两个参数就不报错了

    The argument type 'RefreshConfiguration Function()' can't be assigned to the parameter type 'Widget Function(BuildContext, Widget?)',在main.dart中ScreenUtilInit的builder值报错

    opened by GD-GK 0
  • Error with flutter 2.8.1

    Error with flutter 2.8.1

    The argument type 'RefreshConfiguration Function()' can't be assigned to the parameter type 'Widget Function(BuildContext, Widget?)'. 2022-05-28_211117

    can try adding (BuildContext context, Widget? widget). 2022-05-28_212145

    Note that your app won't be available to users running Android SDKs below 19. 2022-05-28_211533

    can try adding minSdkVersion 19. 2022-05-28_211716

    Your project requires a newer version of the Kotlin Gradle plugin. 2022-05-28_211837

    can try adding ext.kotlin_version = '1.5.30'. 2022-05-28_211952

    opened by tenSunFree 1
Owner
会煮咖啡的猫
会煮咖啡的猫