flutter + getx 最佳實(shí)踐

倉(cāng)庫(kù)地址:github.com/xieyezi/flu…

說在前面

Hi,小伙伴們好久不見,這次帶來一篇flutter + getx 的實(shí)踐文章。

基于getx 實(shí)現(xiàn)的全新flutter getx 模版,適用于中大型項(xiàng)目的開發(fā)。

  • ?? flutter最新版本的空安全
  • ?? view邏輯 完全解耦
  • ? viewstate 自動(dòng)響應(yīng)
  • ?? dioshared_preferences等通用模塊的封裝
  • ?? 去context

環(huán)境

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
復(fù)制代碼

lib目錄劃分

  • common

此目錄用來存放通用模塊及其變量,例如colors、langsvalues等,例如:

├── colors
│   └── colors.dart
├── langs
│   ├── en_US.dart
│   ├── translation_service.dart
│   └── zh_Hans.dart
└── values
    ├── cache.dart
    ├── storage.dart
    └── values.dart
  
復(fù)制代碼
  • components

此目錄主要存放頂層公告組件,例如 appbar、scaffold、dialog等等,例如:

├── components.dart
├── custom_appbar.dart
└── custom_scaffold.dart
復(fù)制代碼
  • pages

此目錄主要存放頁(yè)面文件,例如:

注:每個(gè)Item為一個(gè)文件夾.

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

復(fù)制代碼
  • router

此目錄為路由文件,此模版的路由方式約定為命名路由,為固定目錄,目錄結(jié)構(gòu)如下:

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

復(fù)制代碼
  • services

此目錄用來存放API,例如:

├── services.dart
└── user.dart  // 關(guān)于用戶的API
復(fù)制代碼
  • utils

此目錄用來存放一些工具模塊,例如 requestlocal_storage等等,例如:

├── authentication.dart
├── local_storage.dart
├── request.dart
├── screen_device.dart
└── utils.dart
復(fù)制代碼

開發(fā)規(guī)范

當(dāng)你需要新建一個(gè)頁(yè)面時(shí),你需要按照以下步驟進(jìn)行:

假設(shè)我們現(xiàn)在要?jiǎng)?chuàng)建一個(gè)Home 頁(yè)面.

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

$ mkdir home
$ cd home
復(fù)制代碼
  1. home 目錄下,新建以下四個(gè)文件:
  • home_view.dart : 視圖(用來實(shí)現(xiàn)頁(yè)面布局)
  • home_contrller.dart : 控制器(用來實(shí)現(xiàn)業(yè)務(wù)邏輯)
  • home_binding : 控制器綁定(用來綁定controllerview)
  • home_model : 數(shù)據(jù)模型(用來約定數(shù)據(jù)模型)

注意:每創(chuàng)建一個(gè)頁(yè)面時(shí),都必須如此做,命名采用 '頁(yè)面名_key' 這樣的形式。

當(dāng)你創(chuàng)建好一個(gè)頁(yè)面,目錄應(yīng)該長(zhǎng)這樣??:

// home
.
├── home.binding.dart
├── home_controller.dart
├── home_model.dart
└── home_view.dart
復(fù)制代碼
  1. router文件夾下面添加對(duì)應(yīng)路由:
// app_routes.dart
part of 'app_pages.dart';
abstract class AppRoutes {
  ...
  static const Home = '/home';
  ...
}
復(fù)制代碼
// app_pages.dart
class AppPages {

  static final routes = [
    ...
    GetPage(
      name: AppRoutes.Home,
      page: () => HomePage(),
      binding: HomeBinding(),
    ),
    ...
  ];
}
復(fù)制代碼

完成以上步驟,你就可以愉快的開始開發(fā)了。

狀態(tài)管理

contrller 是我們實(shí)現(xiàn)業(yè)務(wù)邏輯的地方,為什么我們要將 業(yè)務(wù)邏輯和視圖分開呢?因?yàn)?code>flutter 的意大利面式的代碼實(shí)在是太難維護(hù)了,本來flutter 的頁(yè)面布局和樣式寫在一起就很惡心了,再加上業(yè)務(wù)邏輯代碼的話,實(shí)在太難以維護(hù),而且,如果我們想要擁有狀態(tài)的話,我們的頁(yè)面不得不繼承自stateful widget,性能損耗太嚴(yán)重了。

所以我們利用 getx 提供的 controller,將我們的業(yè)務(wù)邏輯和視圖解耦。

一個(gè)標(biāo)準(zhǔn)的contrller長(zhǎng)這樣:

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

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

  @override
  void onReady() {}

  @override
  void onClose() {}

  void increment() => count.value++;
}
復(fù)制代碼

當(dāng)我們需要一個(gè)響應(yīng)式的變量時(shí),我們只需在變量的后面加一個(gè).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;
復(fù)制代碼

值得注意的是,因?yàn)楝F(xiàn)在flutter 有了null-safety,所以我們最好給響應(yīng)式變量一個(gè)初始值。

當(dāng)我們?cè)?code>controller更新了響應(yīng)式變量時(shí),視圖會(huì)自動(dòng)更新渲染。

但是實(shí)際上,你也可以不定義這種響應(yīng)式變量,例如我們可以這樣:

class HomeController extends GetxController {
  int count = 0;

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

  @override
  void onReady() {}

  @override
  void onClose() {}

  void increment() {
    count++;
    update();
  } 
}
復(fù)制代碼

這樣和.obs的唯一區(qū)別是,我們需要手動(dòng)調(diào)用 update() 更新狀態(tài)的變化,這樣view才能在count變化時(shí),收到我們的通知重新渲染。

我們應(yīng)該將發(fā)起請(qǐng)求,放在onInit鉤子里面,例如進(jìn)入訂單頁(yè)面時(shí),我們應(yīng)該獲取訂單信息,就如同在 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(),
    );
  }
}
復(fù)制代碼

GetxView<HomeController> 會(huì)自動(dòng)幫你把 Controller 注入到 view 中,你可以簡(jiǎn)單理解為它自動(dòng)幫你執(zhí)行了以下步驟

final controller = Get.find<HomeController>();
復(fù)制代碼

不必?fù)?dān)心 GetxView<T> 的性能,因?yàn)樗鼉H僅是繼承自 Stateless Widget ,記住,有了 getx 你完全不需要 Stateful Widget

當(dāng)我們想要綁定controller的變量時(shí),我們約定了兩種方法:

  1. Obx(()=>)

如果你的變量是.obs的,那么我們就使用Obx(()=>),它會(huì)在變量變更時(shí)自動(dòng)刷新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++;
}
復(fù)制代碼

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()))),
      ),
    );
  }
}
復(fù)制代碼
  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();
  } 
}
復(fù)制代碼

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('首頁(yè)'),
        leadingType: AppBarBackType.None,
      ),
      body: Container(
        child: GetBuilder<HomeController>(builder: (_) {
          return Center(child: Text(controller.count.toString()));
        }),
      ),
    );
  }
}
復(fù)制代碼

其實(shí)getx還提供了其他的render function,但是為了減少心智負(fù)擔(dān)和復(fù)雜度,我們就使用這兩種就夠了。

路由管理

這里我們采用了getx提供的命名式路由,如果你學(xué)過vue,那么幾乎沒有學(xué)習(xí)成本。

假設(shè)我們現(xiàn)在添加了一個(gè)頁(yè)面,叫做List,然后我們需要到router文件夾下面去配置它:

// app_routes.dart
part of 'app_pages.dart';
abstract class AppRoutes {
  ...
  static const List = '/list';
  ...
}
復(fù)制代碼
// app_pages.dart
class AppPages {

  static final routes = [
    ...
    GetPage(
      name: AppRoutes.Home,
      page: () => ListPage(),
      binding: ListBinding(),
    ),
    ...
  ];
}
復(fù)制代碼

這個(gè)List對(duì)應(yīng)的假設(shè)是訂單列表,當(dāng)我們點(diǎn)擊列表中某個(gè)訂單時(shí),我們通常會(huì)進(jìn)入到訂單詳情頁(yè)面,所以我們此時(shí)應(yīng)再添加一個(gè)詳情頁(yè)面:

// app_routes.dart
part of 'app_pages.dart';
abstract class AppRoutes {
  ...
  static const List = '/list';
  static const Detaul = '/detail';
  ...
}
復(fù)制代碼
// 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(),
        ),
      ],
    ),
    ...
  ];
}
復(fù)制代碼

因?yàn)樵斍轫?yè)面和列表頁(yè)面有先后級(jí)關(guān)系,所以我們可以將 Detail 頁(yè)面,放到 Listchildren 下面,當(dāng)然你也可以不這樣做。

當(dāng)我們使用時(shí):

Get.toNamed('/list/detail');
復(fù)制代碼

其他路由鉤子:

瀏覽并刪除前一個(gè)頁(yè)面:

Get.offNamed("/NextScreen");
復(fù)制代碼

瀏覽并刪除所有以前的頁(yè)面:

Get.offAllNamed("/NextScreen");
復(fù)制代碼

傳遞參數(shù):

Get.toNamed("/NextScreen", arguments: {id: 'xxx'});
復(fù)制代碼

參數(shù)的類型可以是一個(gè)字符串,一個(gè)Map,一個(gè)List,甚至一個(gè)類的實(shí)例。

獲取參數(shù):

print(Get.arguments);
// print out: `{id: 'xxx'}`
復(fù)制代碼

使用 getx 的路由它有一個(gè)非常好的優(yōu)點(diǎn),那就是它是去context化的。還記得我們以前被context 支配的恐懼嗎? 有了getx,它將不復(fù)存在。

使用 monia-cli 進(jìn)行開發(fā)

我們很高興,能將 flutter-getx-template 加入到 monia-cli。

利用 monia-cli 新建flutter項(xiàng)目:

monia create <project-name>
復(fù)制代碼
?  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

                        _                  _ _ 
  _ __ ___   ___  _ __ (_) __ _        ___| (_)
 | '_ ` _ \ / _ \| '_ \| |/ _` |_____ / __| | |
 | | | | | | (_) | | | | | (_| |_____| (__| | |
 |_| |_| |_|\___/|_| |_|_|\__,_|      \___|_|_|
復(fù)制代碼

不僅如此, monia-cli 還提供了快速生成一個(gè) flutter getx 頁(yè)面的功能。

假如現(xiàn)在你想生成一個(gè) order_sending 新頁(yè)面,你只需在 pages 目錄下面輸入:

monia init order_sending
復(fù)制代碼
?  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.

復(fù)制代碼

vscode 插件

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

點(diǎn)擊左下角的monia-generate 文字按鈕,輸入pageName,即可在pages目錄下新建一個(gè)flutter getx page

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容