flutter + getx 最佳實踐

覺非發表於2021-08-17

倉庫地址: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

官方連結

狀態管理 路由管理 依賴管理

相關文章