說在前面
Hi,小夥伴們好久不見,這次帶來一篇flutter + getx 的實踐文章。
基於getx
實現的全新flutter getx
模版,適用於中大型專案的開發。
- ?
flutter
最新版本的空安全 - ?
view
和邏輯
完全解耦 - ⚡
view
和state
自動響應 - ?
dio
、shared_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
此目錄用來存放通用模組及其變數,例如colors
、langs
、values
等,例如:
├── colors
│ └── colors.dart
├── langs
│ ├── en_US.dart
│ ├── translation_service.dart
│ └── zh_Hans.dart
└── values
├── cache.dart
├── storage.dart
└── values.dart
複製程式碼
components
此目錄主要存放頂層公告元件,例如 appbar
、scaffold
、dialog
等等,例如:
├── 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
此目錄用來存放一些工具模組,例如 request
、local_storage
等等,例如:
├── authentication.dart
├── local_storage.dart
├── request.dart
├── screen_device.dart
└── utils.dart
複製程式碼
開發規範
當你需要新建一個頁面時,你需要按照以下步驟進行:
假設我們現在要建立一個
Home
頁面.
- 在
pages
目錄下新建home
目錄:
// pages
$ mkdir home
$ cd home
複製程式碼
- 在
home
目錄下,新建以下四個檔案:
-
home_view.dart
: 檢視(用來實現頁面佈局) -
home_contrller.dart
: 控制器(用來實現業務邏輯) -
home_binding
: 控制器繫結(用來繫結controller
到view
) -
home_model
: 資料模型(用來約定資料模型)
注意:每建立一個頁面時,都必須如此做,命名採用 '頁面名_key
' 這樣的形式。
當你建立好一個頁面,目錄應該長這樣?:
// home
.
├── home.binding.dart
├── home_controller.dart
├── home_model.dart
└── home_view.dart
複製程式碼
- 到
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
的變數時,我們約定了兩種方法:
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()))),
),
);
}
}
複製程式碼
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
頁面,放到 List
的children
下面,當然你也可以不這樣做。
當我們使用時:
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
: