視圖與邏輯分離之道序篇-使用MVVM模式管理狀態(GetState)

瞭解 GetState

❓ 爲何作GetState

Flutter 狀態管理方案百花齊放, 從 ScopeModel 到 Provide、MobX, 再到 BLoC、Redux、Provider. 特別是BLoC和Provider, 已經有了大量的用戶,可是我在實際使用的時候,發現了這樣幾個問題:git

  • 使用不便,須要手動編寫大量的樣板代碼,狀態都須要手動註冊
  • 業務邏輯與UI表現邏輯, 甚至直接與UI耦合。
  • 面對大型項目沒法清晰的爲各層次劃清界限, 單元測試代碼編寫繁瑣。

面對這些問題,GetState應運而生github

  • 自動註冊狀態: 解放雙手, 保護頭髮
  • 極致的速度: GetState提供時間複雜度爲O(1)的訪問性能, 暴打一衆O(N)的狀態管理方案
  • 便於單元測試: 業務邏輯與UI代碼解耦, 媽媽不再用擔憂個人單元測試了, 保護頭髮*2
  • 狀態時光機: 使用Recorder, 在過去與如今之間穿梭

GetState目前仍然有不少不足之處, 但願你們多多PR, issue 💕markdown

GetState : 致力於解決Flutter應用UI與業務邏輯解耦問題的MVVM狀態管理方案app

進入正題

🛸 先放上 Pub 以及 項目地址

歡迎Star, PR, issue 😘less

前三個Demo分別介紹ViewModel,View和Model,心急的能夠直接跳過, 或者配合教程3閱讀Demo3異步

如下是教程中的Demo源碼async


🛴瞭解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, 直接複製粘貼是沒法運行的, 具體緣由是由於沒有爲本身生成相應的 依賴注入代碼



但願各位多多點贊支持, 更歡迎你們提出意見與建議😀

有時間的話會補上後3個教程的😜

✨✨

後續

  • 關於上文中留下的問題

"💡 猜一猜複雜的業務邏輯應該怎麼處理", 請參見GetArch介紹

  • GetState 新版本已支持View級ViewModel自動註冊, 相比頁面級註冊, 使用更方便,詳見 項目 中的example
相關文章
相關標籤/搜索