flutter + getx 最佳实践

18,741

仓库地址:github.com/xieyezi/flu…

说在前面

Hi,小伙伴们好久不见,这次带来一篇flutter + getx 的实践文章。

基于getx 实现的全新flutter getx 模版,适用于中大型项目的开发。

  • 💥 flutter最新版本的空安全
  • 🍀 view逻辑 完全解耦
  • viewstate 自动响应
  • 📦 dioshared_preferences等通用模块的封装
  • 🔥 去context

环境

Flutter 2.2.0 • channel stable • https://github.com/flutter/flutter.git
Framework • revision b22742018b (3 weeks ago)2021-05-14 19:12:57 -0700
Engine • revision a9d88a4d18
Tools • Dart 2.13.0

lib目录划分

  • common

此目录用来存放通用模块及其变量,例如colorslangsvalues等,例如:

├── colors
│   └── colors.dart
├── langs
│   ├── en_US.dart
│   ├── translation_service.dart
│   └── zh_Hans.dart
└── values
    ├── cache.dart
    ├── storage.dart
    └── values.dart
  
  • components

此目录主要存放顶层公告组件,例如 appbarscaffolddialog等等,例如:

├── components.dart
├── custom_appbar.dart
└── custom_scaffold.dart
  • pages

此目录主要存放页面文件,例如:

注:每个Item为一个文件夹.

├── Index
├── home
├── login
├── notfound
├── proxy
└── splash

  • router

此目录为路由文件,此模版的路由方式约定为命名路由,为固定目录,目录结构如下:

├── app_pages.dart
└── app_routes.dart

  • services

此目录用来存放API,例如:

├── services.dart
└── user.dart  // 关于用户的API
  • utils

此目录用来存放一些工具模块,例如 requestlocal_storage等等,例如:

├── authentication.dart
├── local_storage.dart
├── request.dart
├── screen_device.dart
└── utils.dart

开发规范

当你需要新建一个页面时,你需要按照以下步骤进行:

假设我们现在要创建一个Home 页面.

  1. pages 目录下新建home 目录:
// pages

$ mkdir home
$ cd home
  1. home 目录下,新建以下四个文件:
  • home_view.dart : 视图(用来实现页面布局)

  • home_contrller.dart : 控制器(用来实现业务逻辑)

  • home_binding : 控制器绑定(用来绑定controllerview)

  • home_model : 数据模型(用来约定数据模型)

注意:每创建一个页面时,都必须如此做,命名采用 '页面名_key' 这样的形式。

当你创建好一个页面,目录应该长这样👇:

// home
.
├── home.binding.dart
├── home_controller.dart
├── home_model.dart
└── home_view.dart
  1. router文件夹下面添加对应路由:
// app_routes.dart
part of 'app_pages.dart';
abstract class AppRoutes {
  ...
  static const Home = '/home';
  ...
}
// app_pages.dart
class AppPages {

  static final routes = [
    ...
    GetPage(
      name: AppRoutes.Home,
      page: () => HomePage(),
      binding: HomeBinding(),
    ),
    ...
  ];
}

完成以上步骤,你就可以愉快的开始开发了。

状态管理

contrller 是我们实现业务逻辑的地方,为什么我们要将 业务逻辑和视图分开呢?因为flutter 的意大利面式的代码实在是太难维护了,本来flutter 的页面布局和样式写在一起就很恶心了,再加上业务逻辑代码的话,实在太难以维护,而且,如果我们想要拥有状态的话,我们的页面不得不继承自stateful widget,性能损耗太严重了。

所以我们利用 getx 提供的 controller,将我们的业务逻辑和视图解耦。

一个标准的contrller长这样:

class HomeController extends GetxController {
  final count = 0.obs;

  @override
  void onInit() {
    super.onInit();
  }

  @override
  void onReady() {}

  @override
  void onClose() {}

  void increment() => count.value++;
}

当我们需要一个响应式的变量时,我们只需在变量的后面加一个.obs,例如:

final name = ''.obs;
final isLogged = false.obs;
final count = 0.obs;
final balance = 0.0.obs;
final number = 0.obs;
final items = <String>[].obs;
final myMap = <String, int>{}.obs;

// 甚至自定义类 - 可以是任何类
final user = User().obs;

值得注意的是,因为现在flutter 有了null-safety,所以我们最好给响应式变量一个初始值。

当我们在controller更新了响应式变量时,视图会自动更新渲染。

但是实际上,你也可以不定义这种响应式变量,例如我们可以这样:

class HomeController extends GetxController {
  int count = 0;

  @override
  void onInit() {
    super.onInit();
  }

  @override
  void onReady() {}

  @override
  void onClose() {}

  void increment() {
    count++;
    update();
  } 
}

这样和.obs的唯一区别是,我们需要手动调用 update() 更新状态的变化,这样view才能在count变化时,收到我们的通知重新渲染。

我们应该将发起请求,放在onInit钩子里面,例如进入订单页面时,我们应该获取订单信息,就如同在 stateful wdiget 里面的init钩子一样。

视图

首先,你需要将你的class 继承自 GetxView<T>(T 为你的Controller),例如:

class HomePage extends GetView<HomeController> {
  HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(),
    );
  }
}

GetxView<HomeController> 会自动帮你把 Controller 注入到 view 中,你可以简单理解为它自动帮你执行了以下步骤

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

不必担心 GetxView<T> 的性能,因为它仅仅是继承自 Stateless Widget ,记住,有了 getx 你完全不需要 Stateful Widget

当我们想要绑定controller的变量时,我们约定了两种方法:

  1. Obx(()=>)

如果你的变量是.obs的,那么我们就使用Obx(()=>),它会在变量变更时自动刷新view,例如:

// home_contrller
class HomeController extends GetxController {
  final count = 0.obs;

  @override
  void onInit() {
    super.onInit();
  }

  @override
  void onReady() {}

  @override
  void onClose() {}

  void increment() => count.value++;
}

view里面使用 Obx(()=>) 绑定count:

// home_view
class HomePage extends GetView<HomeController> {
  HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        child: Obx(() => Center(child: Text(controller.count.toString()))),
      ),
    );
  }
}
  1. GetBuilder<T>

如果你的变量不是.obs的,那么我们就使用GetBuilder<T>,例如:

class HomeController extends GetxController {
  int count = 0;

  @override
  void onInit() {
    super.onInit();
  }

  @override
  void onReady() {}

  @override
  void onClose() {}

  void increment() {
    count++;
    update();
  } 
}

view 里面使用 GetBuilder<T> 绑定count:

class HomePage extends GetView<HomeController> {
  HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BaseScaffold(
      appBar: MyAppBar(
        centerTitle: true,
        title: MyTitle('首页'),
        leadingType: AppBarBackType.None,
      ),
      body: Container(
        child: GetBuilder<HomeController>(builder: (_) {
          return Center(child: Text(controller.count.toString()));
        }),
      ),
    );
  }
}

其实getx还提供了其他的render function,但是为了减少心智负担和复杂度,我们就使用这两种就够了。

路由管理

这里我们采用了getx提供的命名式路由,如果你学过vue,那么几乎没有学习成本。

假设我们现在添加了一个页面,叫做List,然后我们需要到router文件夹下面去配置它:

// app_routes.dart
part of 'app_pages.dart';
abstract class AppRoutes {
  ...
  static const List = '/list';
  ...
}
// app_pages.dart
class AppPages {

  static final routes = [
    ...
    GetPage(
      name: AppRoutes.Home,
      page: () => ListPage(),
      binding: ListBinding(),
    ),
    ...
  ];
}

这个List对应的假设是订单列表,当我们点击列表中某个订单时,我们通常会进入到订单详情页面,所以我们此时应再添加一个详情页面:

// app_routes.dart
part of 'app_pages.dart';
abstract class AppRoutes {
  ...
  static const List = '/list';
  static const Detaul = '/detail';
  ...
}
// app_pages.dart
class AppPages {

  static final routes = [
    ...
    GetPage(
      name: AppRoutes.Home,
      page: () => ListPage(),
      binding: ListBinding(),
      children: [
        GetPage(
          name: AppRoutes.Detail,
          page: () => DetailPage(),
          binding: DetailBinding(),
        ),
      ],
    ),
    ...
  ];
}

因为详情页面和列表页面有先后级关系,所以我们可以将 Detail 页面,放到 Listchildren 下面,当然你也可以不这样做。

当我们使用时:

Get.toNamed('/list/detail');

其他路由钩子:

浏览并删除前一个页面:

Get.offNamed("/NextScreen");

浏览并删除所有以前的页面:

Get.offAllNamed("/NextScreen");

传递参数:

Get.toNamed("/NextScreen", arguments: {id: 'xxx'});

参数的类型可以是一个字符串,一个Map,一个List,甚至一个类的实例。

获取参数:

print(Get.arguments);
// print out: `{id: 'xxx'}`

使用 getx 的路由它有一个非常好的优点,那就是它是去context化的。还记得我们以前被context 支配的恐惧吗? 有了getx,它将不复存在。

使用 monia-cli 进行开发

我们很高兴,能将 flutter-getx-template 加入到 monia-cli

利用 monia-cli 新建flutter项目:

monia create <project-name>
➜  Desktop monia create flutter_demo
? Which framework do you want to create Flutter
? Which flutter version do you want to create null-safety
? Please input your project description description
? Please input project version 1.0.0

✨  Creating project in /Users/xieyezi/Desktop/flutter_demo.

🗃  Initializing git repository....
.......
⠏ Download template from monia git repository... This might take a while....

🎉  Successfully created project flutter_demo.
👉  Get started with the following commands:

$ cd flutter_demo
$ flutter run

                        _                  _ _ 
  _ __ ___   ___  _ __ (_) __ _        ___| (_)
 | '_ ` _ \ / _ \| '_ \| |/ _` |_____ / __| | |
 | | | | | | (_) | | | | | (_| |_____| (__| | |
 |_| |_| |_|\___/|_| |_|_|\__,_|      \___|_|_|

不仅如此, monia-cli 还提供了快速生成一个 flutter getx 页面的功能。

假如现在你想生成一个 order_sending 新页面,你只需在 pages 目录下面输入:

monia init order_sending
➜  pages monia init order_sending
✨  Generate page in /Users/xieyezi/Desktop/flutter_demo/lib/pages/order_sending.
⠋ Generating, it's will not be wait long...
generate order_sending lib success.
generate /Users/xieyezi/Desktop/flutter_demo/lib/pages/order_sending/order_sending_view.dart file success.
generate /Users/xieyezi/Desktop/flutter_demo/lib/pages/order_sending/order_sending_controller.dart file success.
generate /Users/xieyezi/Desktop/flutter_demo/lib/pages/order_sending/order_sending_binding.dart file success.

🎉  Successfully generate page order_sending.

vscode 插件

monia 还提供了vscode 插件: monia-vscode-extension

点击左下角的monia-generate 文字按钮,输入pageName,即可在pages目录下新建一个flutter getx page

example.gif

官方链接

状态管理 路由管理 依赖管理