瞭解 GetState
❓ 為什麼做GetState
Flutter 狀態管理方案百花齊放, 從 ScopeModel 到 Provide、MobX, 再到 BLoC、Redux、Provider. 特別是BLoC和Provider, 已經有了大量的使用者,但是我在實際使用的時候,發現了這樣幾個問題:
- 使用不便,需要手動編寫大量的樣板程式碼,狀態都需要手動註冊
- 業務邏輯與UI表現邏輯, 甚至直接與UI耦合。
- 面對大型專案無法清晰的為各層次劃清界限, 單元測試程式碼編寫繁瑣。
面對這些問題,GetState應運而生
- 自動註冊狀態: 解放雙手, 保護頭髮
- 極致的速度: GetState提供時間複雜度為O(1)的訪問效能, 暴打一眾O(N)的狀態管理方案
- 便於單元測試: 業務邏輯與UI程式碼解耦, 媽媽再也不用擔心我的單元測試了, 保護頭髮*2
- 狀態時光機: 使用Recorder, 在過去與現在之間穿梭
GetState目前仍然有很多不足之處, 希望大家多多PR, issue ?
GetState : 致力於解決Flutter應用UI與業務邏輯解耦問題的MVVM狀態管理方案
進入正題
? 先放上 Pub 以及 專案地址
歡迎Star, PR, issue ?
前三個Demo分別介紹ViewModel,View和Model,心急的可以直接跳過, 或者配合教程3閱讀Demo3
以下是教程中的Demo原始碼
-
? 瞭解原理 ViewModel的作用: ViewModel登場.dart
-
? 包裝一個View: 帶上View.dart
-
? 自定義 Model: M, V, VM一家要整整齊齊.dart
-
? 司機上路: 半自動註冊狀態與跨頁狀態修改.dart
?瞭解GetState原理 - ViewModel的作用 (Demo0)
按照Flutter的慣例, 第一個Demo當然是選擇經典的CounterApp了
? 不推薦本例中的寫法, Demo僅供瞭解GetState原理
0-確保配置yaml配置正確
dependencies:
flutter:
sdk: flutter
## 引入get_state
get_state: <這裡填寫版本號>
複製程式碼
1-編寫viewmodel類-countervm
ViewModel負責簡單的業務邏輯和操作檢視
? 猜一猜複雜的業務邏輯應該怎麼處理
這裡的操作Model的方法(如incrementCounter),相當於BLoC中的Event
ViewModel的泛型即Model的型別, 這裡直接使用int型別, 當然也可以使用自定義型別, 詳見後面"推薦用法"
class CounterVm extends ViewModel<int> {
// 1.1 在ViewModel的構造中, 提供預設的初始值
CounterVm() : super(initModel: 0);
// 1.2 獲取Model方法, 這裡的model時父類中的屬性,其型別用本類泛型指定
int counter()=> m;
// 1.3 操作Model方法,
// 呼叫 父類中的vmUpdate(M m)方法更新model的值
void incrementCounter() {
vmUpdate(m + 1);
}
}
複製程式碼
2-在main方法中註冊ViewModel(手動註冊方式)
? 既然有"手動註冊"方式, 那麼肯定有自動註冊方式了, 詳見後面的程式碼
使用 GetIt g = GetIt.instance; 獲取GetIt例項.
實際上直接使用GetIt.instance或GetIt.I效果是一樣的,且它們都是單例模式. 這裡將其賦值給 g,只是為了便於使用.
當然, 推薦命名為 _g
新增 WidgetsFlutterBinding.ensureInitialized();以防止ViewModel註冊失敗
關於WidgetsFlutterBinding.ensureInitialized()的作用,這裡貼出Flutter原始碼中的說明
"You only need to call this method if you need the binding to be initialized before calling [runApp]."
使用 GetIt.I.registerSingleton<泛型>(構造方法); 以懶單例的方式註冊ViewModel
get_it 還有更多註冊方式, 這裡暫時只介紹懶單例註冊方式
GetIt g = GetIt.instance;
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// 4.手動注入依賴, 確保View可以獲取到ViewModel
g.registerSingleton<CounterVm>(CounterVm());
runApp(MyApp());
}
複製程式碼
3-最後,在UI程式碼中呼叫ViewMdoel的方法來操作與獲取資料
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) => MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('演示:0.極簡使用方法'),
),
body: Center(
child: Text('測試0: ${g<CounterVm>().counter()}'),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () => g<CounterVm>().incrementCounter(),
),
),
);
}
複製程式碼
Demo1到此結束了, 本例僅供瞭解GetState原理, 實際使用中, 不建議使用這樣的寫法.標準寫法見Demo3
接下來是包裝View的Demo.
? 包裝一個View (Demo1)
直接將ViewModel和GetIt例項裸露在外一點也不優雅, 如果封裝為View使用起來可就方便多了
0-先確保配置了yaml
yaml內容 跟Demo0一樣
1-再編寫ViewModel
這裡直接使用Demo0中的ViewModel
2-編寫View類(MyCounterView)
View類只負責檢視展示, 儘量將操作檢視的程式碼移動到 ViewModel中
View就是最終展示出來的Widget
class MyCounterView extends View<MyCounterViewModel> {
@override
Widget build(BuildContext c, MyCounterViewModel vm) => ListTile(
title: Text('測試1: ${vm.counter}'),
trailing: RaisedButton(
child: Icon(Icons.add),
onPressed: () => vm.incrementCounter(),
),
);
}
複製程式碼
3-將View放到Widget樹中
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) => MaterialApp(
title: '演示:1.初級使用方法',
home: Scaffold(
appBar: AppBar(),
body: Column(children: <Widget>[
// 將檢視放入需要的地方
MyCounterView(),
]),
),
);
}
複製程式碼
3-在main方法中註冊依賴
這裡還是沿用 Demo0中的方法
包裝View的Demo到此結束, 這樣的寫法適用於Model十分簡單的情況, 但實際上如果Model十分簡單, 也就失去使用狀態管理的意義了, 圖一樂也就圖一樂,真圖一樂還得看Demo3
? 自定義 Model (Demo2)
在實際應用中, Model肯定不會是一個基本型別, 否則也就失去使用狀態管理的意義了
✨ 建議自己動手的時候也按照本文中的步驟操作
0-先確保配置了yaml
dependencies:
flutter:
sdk: flutter
## 1. 引入get_state
get_state: ^3.3.0
## 2- 可以通過引入equatable,省去手動覆寫==和hashCode
equatable: ^1.1.1
複製程式碼
1-編寫Model(CounterModel)
建立一個簡單的狀態, 內部有兩個變數 number和str
Model有兩種寫法, 其實本質上沒有區別, 先看看寫法1
/// 寫法1
class CounterModel {
final int number;
final String str;
CounterModel(this.number, this.str);
// todo 注意, 這裡務必覆寫==與hashCode, 否則無法正常重新整理
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CounterModel &&
runtimeType == other.runtimeType &&
number == other.number &&
str == other.str;
@override
int get hashCode => number.hashCode ^ str.hashCode;
}
複製程式碼
✨ 這裡推薦寫法2, 使用Equatable貫徹"解放雙手,保護頭髮"的理念.
雖然有IDE加持, 覆寫==與hashCode方法並一般不費時間.
但如果Model中的欄位很多,頻繁修改欄位的同時, 還要修改 ==與hashCode方法, 太過麻煩.
/// 寫法2: 使用 Equatable
class CounterModel2 extends Equatable {
final int number;
final String str;
CounterModel2(this.number, this.str);
// todo 這裡需要將所有的屬性值都放入 props中
@override
List<Object> get props => [number, str];
// ✨ 小技巧, 新增下面這行程式碼,連toString都不用動手了
@override
final stringify = true;
}
複製程式碼
2-編寫ViewModel(CounterVm)
這裡沿用Demo0中的程式碼
3-編寫View(MyCounterView)
這裡沿用Demo1中的View
4-再將View放入Widget樹
仍然沿用Demo1中的程式碼
5-最後不要忘記註冊依賴(自動註冊就不用考慮這一步了)
還是用Demo0中的依賴註冊方式
GetState基礎使用教程至此結束, 是不是十分簡單呢? ?
? 半自動註冊狀態與跨頁狀態修改 (Demo3)
? emmm, 不用多說, 肯定有全自動註冊的方法了, 不過由於篇幅有限, 全自動註冊的方法請參考 這裡, 這裡不再做詳細說明(不建議新手使用)
0-先確保配置了yaml
❗ 這裡的yaml與之前的相差較大, 注意觀察
dependencies:
flutter:
sdk: flutter
## 1. 引入get_state
get_state: ^3.3.0
## 2- 可以通過引入equatable,省去手動覆寫==和hashCode
equatable: ^1.2.0
## 3- 通過injectable省去手動註冊步驟
injectable: ^0.4.0+1
dev_dependencies:
flutter_test:
sdk: flutter
## 4- injectable需要額外新增下面兩個依賴
build_runner: ^1.10.0
## 5- 這個同樣重要
injectable_generator: ^0.4.1
複製程式碼
1-1頁面A-建立Model(CounterModel2)
本Demo將會建立兩個Page, 先看第一個頁面.
Model內容與上一個Demo中的CounterModel基本一致
class CounterModel2 extends Equatable {
final int number;
final String str;
CounterModel2(this.number, this.str);
// 1. 這裡需要將所有的屬性值都放入 props中
@override
List<Object> get props => [number, str];
}
複製程式碼
1-2頁面A-建立ViewModel(MyCounterViewModel)
? 這裡要注意, 一定要新增"@lazySingleton"註解, 這就是"半自動"的一部分, 千萬不要省略
不是光加上註解的完事了, "半自動"還有另一半操作呢?
@lazySingleton
class MyCounterViewModel extends ViewModel<CounterModel2> {
MyCounterViewModel() : super(initModel: CounterModel2(3, '- -'));
int get counter => m.number;
void incrementCounter() {
vmUpdate(CounterModel2(m.number + 1, '新的值'));
}
}
複製程式碼
1-3頁面A-建立View(MyCounterView)
class MyCounterView extends View<MyCounterViewModel> {
@override
Widget build(BuildContext c, MyCounterViewModel vm) => ListTile(
leading: Text('測試3: ${vm.counter}'),
title: Text('${vm.m.str}'),
trailing: RaisedButton(
child: Icon(Icons.add),
onPressed: () => vm.incrementCounter(),
),
);
}
複製程式碼
1-4頁面A-將View放到Page中
這裡的MapApp 跟前面的不太一樣, 不要太在意這些細節, 問題不大
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: Text('演示:3.標準使用方法'),
),
body: Column(children: <Widget>[
// View 1
MyCounterView(),
RaisedButton(
child: Text('跳轉到新頁面'),
onPressed: () => Navigator.of(context).push(MaterialPageRoute(
builder: (c) => Page2(),
)),
),
RaisedButton(
child: Text('點選更改另一個頁面的值'),
onPressed: () => g<Pg2Vm>().add,
),
]),
);
}
複製程式碼
看這裡, "跨頁修改狀態"就是這麼簡單粗暴 ?
RaisedButton(
child: Text('點選更改另一個頁面的值'),
onPressed: () => g<Pg2Vm>().add,
),
複製程式碼
2-1頁面B-建立Model
頁面1的MVVM一家已經建立完畢了, 頁面2只是為了演示跨頁狀態的修改, 所以就隨便寫一下
// 你沒看錯, 頁面2不定義Model了, 直接用int型別吧
複製程式碼
2-2頁面B-建立ViewModel(Pg2Vm)
跟上面一樣, 同樣不要忘記加上"@lazySingleton"
@lazySingleton
class Pg2Vm extends ViewModel<int> {
Pg2Vm() : super(initModel: 3);
String get strVal => "$m";
get add => vmUpdate(m + 1);
}
複製程式碼
2-3頁面B-建立View(FooView)
再建立一個簡單的View, 包裝以下ViewModel
class FooView extends View<Pg2Vm> {
@override
Widget build(BuildContext c, Pg2Vm vm) => RaisedButton(
child: Text('${vm.strVal}'),
onPressed: () => vm.add,
);
}
複製程式碼
2-4頁面B-將View放入Page中
class Page2 extends StatelessWidget{
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(),
body: Center(
child: FooView(),
),
);
}
複製程式碼
3-1初始化Injectable
將下面的函式直接寫在main.dart檔案裡面, 當然,另外建立一個新dart檔案也可以, 問題不大.
函式, 一定要放在類的外面, 放在類裡面的叫方法.
同樣不要放了新增註解"@injectableInit".
建議直接複製下面的程式碼到自己專案裡
寫好之後, IDE會提示"找不到$initGetIt"函式, 不要著急, 這個函式還沒有自動生成呢
// 新增註解
@injectableInit
Future<void> configDi() async {
$initGetIt(g);
}
複製程式碼
❗ 注意,這裡的 configDi方法返回值是 Future, 但是函式體內沒有await.
這是因為當前生成的依賴注入程式碼都是同步的, 如果用到了@preResolve註解, 則生成的 $initGetIt()是一個非同步方法, 必須要加上await,否則會出錯
3-2自動生成注入程式碼
開啟Terminal(或者用CMD進入專案的lib同級路徑), 輸入
flutter pub run build_runner build --delete-conflicting-outputs
複製程式碼
如果希望build_runner在後臺持續自動生成程式碼,則輸入
flutter pub run build_runner watch --delete-conflicting-outputs
複製程式碼
這裡的"--delete-conflicting-outputs"表示清除已經生成過的程式碼, 如果你之前已經生成過程式碼, 而第二次生成又不想重新開始, 則可以不加這個引數
如果生成失敗, 注意檢視錯誤程式碼, 一般情況下加上"--delete-conflicting-outputs"就能解決問題
待程式碼生成完畢後, 在原本報錯的程式碼處import新生成的 xxx.iconfig.dart檔案就可以了.
4-在main中新增依賴注入
GetIt g = GetIt.instance;
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// 5. 新增自動依賴注入
configDi();
runApp(MaterialApp(home: MyApp()));
}
複製程式碼
???大功告成???
以上Demo就是get_state的一般用法了, 不過除此之外, get_state還有更多技巧等待你的解鎖?
下面幾個Demo的依賴於這個檔案.dart, 直接複製貼上是無法執行的, 具體原因是因為沒有為自己生成相應的 依賴注入程式碼
-
? 頁面級註冊: 進入頁面時註冊狀態,退出即銷燬.dart
-
? ViewModel非同步初始化: 其實我覺得這個功能用處不大.dart
-
? 狀態時光機 在過去與現在之間反覆橫跳.dart
希望各位多多點贊支援, 更歡迎大家提出意見與建議?
有時間的話會補上後3個教程的?
✨✨
後續
- 關於上文中留下的問題
"? 猜一猜複雜的業務邏輯應該怎麼處理", 請參見GetArch介紹
- GetState 新版本已支援View級ViewModel自動註冊, 相比頁面級註冊, 使用更方便,詳見 專案 中的example