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

了解 GetState

? 為什么做GetState

Flutter 狀態(tài)管理方案百花齊放, 從 ScopeModel 到 Provide尚洽、MobX, 再到 BLoC疑苫、Redux银舱、Provider. 特別是BLoC和Provider值骇, 已經(jīng)有了大量的用戶,但是我在實(shí)際使用的時(shí)候因苹,發(fā)現(xiàn)了這樣幾個(gè)問題:

  • 使用不便,需要手動(dòng)編寫大量的樣板代碼篇恒,狀態(tài)都需要手動(dòng)注冊(cè)
  • 業(yè)務(wù)邏輯與UI表現(xiàn)邏輯, 甚至直接與UI耦合扶檐。
  • 面對(duì)大型項(xiàng)目無(wú)法清晰的為各層次劃清界限, 單元測(cè)試代碼編寫繁瑣。

面對(duì)這些問題胁艰,GetState應(yīng)運(yùn)而生

  • 自動(dòng)注冊(cè)狀態(tài): 解放雙手, 保護(hù)頭發(fā)
  • 極致的速度: GetState提供時(shí)間復(fù)雜度為O(1)的訪問性能, 暴打一眾O(N)的狀態(tài)管理方案
  • 便于單元測(cè)試: 業(yè)務(wù)邏輯與UI代碼解耦, 媽媽再也不用擔(dān)心我的單元測(cè)試了, 保護(hù)頭發(fā)*2
  • 狀態(tài)時(shí)光機(jī): 使用Recorder, 在過去與現(xiàn)在之間穿梭
  • 使用靈活: 既支持Provider使用的mutable狀態(tài)款筑,也支持BLoC,Redux使用的immutable狀態(tài)
  • 強(qiáng)大的兼容性: 如果你已經(jīng)使用了Provider, Redux, BLoC等狀態(tài)管理方案, 那么切換到GetState, 你并不需要移除已有的狀態(tài)管理代碼, GetState可以與現(xiàn)有的狀態(tài)管理方案共存.
    </br> </br>
    </br> </br>

GetState : 致力于解決Flutter應(yīng)用UI與業(yè)務(wù)邏輯解耦問題的MVVM狀態(tài)管理方案

</br> </br>

進(jìn)入正題

?? 先放上 Pub 以及 項(xiàng)目地址

歡迎Star, PR, issue ??

前三個(gè)Demo分別介紹ViewModel,View和Model,心急的可以直接跳過, 或者配合教程3閱讀Demo3

以下是教程中的Demo源碼

</br>

??了解GetState原理 - ViewModel的作用 (Demo0)

按照Flutter的慣例, 第一個(gè)Demo當(dāng)然是選擇經(jīng)典的CounterApp了

?? 不推薦本例中的寫法, Demo僅供了解GetState原理

0-確保配置yaml配置正確

dependencies:
  flutter:
    sdk: flutter
  ## 引入get_state
  get_state: <這里填寫版本號(hào)>

1-編寫viewmodel類-countervm

ViewModel負(fù)責(zé)簡(jiǎn)單的業(yè)務(wù)邏輯和操作視圖

?? 猜一猜復(fù)雜的業(yè)務(wù)邏輯應(yīng)該怎么處理

這里的操作Model的方法(如incrementCounter),相當(dāng)于BLoC中的Event

ViewModel的泛型即Model的類型, 這里直接使用int類型, 當(dāng)然也可以使用自定義類型, 詳見后面"推薦用法"

class CounterVm extends ViewModel<int> {
  // 1.1 在ViewModel的構(gòu)造中, 提供默認(rèn)的初始值
  CounterVm() : super(initModel: 0);

  // 1.2 獲取Model方法, 這里的model時(shí)父類中的屬性,其類型用本類泛型指定
  int counter()=> m;

  // 1.3 操作Model方法,
  // 調(diào)用 父類中的vmUpdate(M m)方法更新model的值
  void incrementCounter() {
    vmUpdate(m + 1);
  }
}

2-在main方法中注冊(cè)ViewModel(手動(dòng)注冊(cè)方式)

?? 既然有"手動(dòng)注冊(cè)"方式, 那么肯定有自動(dòng)注冊(cè)方式了, 詳見后面的代碼

使用 GetIt g = GetIt.instance; 獲取GetIt實(shí)例.

實(shí)際上直接使用GetIt.instance或GetIt.I效果是一樣的,且它們都是單例模式. 這里將其賦值給 g,只是為了便于使用.
當(dāng)然, 推薦命名為 _g

添加 WidgetsFlutterBinding.ensureInitialized();以防止ViewModel注冊(cè)失敗

關(guān)于WidgetsFlutterBinding.ensureInitialized()的作用,這里貼出Flutter源碼中的說明
"You only need to call this method if you need the binding to be initialized before calling [runApp]."

使用 GetIt.I.registerSingleton<泛型>(構(gòu)造方法); 以懶單例的方式注冊(cè)ViewModel

get_it 還有更多注冊(cè)方式, 這里暫時(shí)只介紹懶單例注冊(cè)方式

GetIt g = GetIt.instance;

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  // 4.手動(dòng)注入依賴, 確保View可以獲取到ViewModel
  g.registerSingleton<CounterVm>(CounterVm());
  runApp(MyApp());
}

3-最后,在UI代碼中調(diào)用ViewMdoel的方法來(lái)操作與獲取數(shù)據(jù)

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) => MaterialApp(
        home: Scaffold(
          appBar: AppBar(
            title: Text('演示:0.極簡(jiǎn)使用方法'),
          ),
          body: Center(
            child: Text('測(cè)試0: ${g<CounterVm>().counter()}'),
          ),
          floatingActionButton: FloatingActionButton(
            child: Icon(Icons.add),
            onPressed: () => g<CounterVm>().incrementCounter(),
          ),
        ),
      );
}

Demo1到此結(jié)束了, 本例僅供了解GetState原理, 實(shí)際使用中, 不建議使用這樣的寫法.標(biāo)準(zhǔn)寫法見Demo3
接下來(lái)是包裝View的Demo.

</br></br></br>


</br>

?? 包裝一個(gè)View (Demo1)

直接將ViewModel和GetIt實(shí)例裸露在外一點(diǎn)也不優(yōu)雅, 如果封裝為View使用起來(lái)可就方便多了

0-先確保配置了yaml

yaml內(nèi)容 跟Demo0一樣

1-再編寫ViewModel

這里直接使用Demo0中的ViewModel

2-編寫View類(MyCounterView)

View類負(fù)責(zé)UI繪制, 控制UI的邏輯應(yīng)當(dāng)盡量放在View里面, 業(yè)務(wù)邏輯可以放在ViewModel中,
一個(gè)ViewModel經(jīng)常會(huì)對(duì)應(yīng)多個(gè)View, 根據(jù)迪米特原則, 各個(gè)View應(yīng)當(dāng)在其內(nèi)部處理好UI繪制邏輯.

View就是最終展示出來(lái)的Widget

class MyCounterView extends View<MyCounterViewModel> {
  @override
  Widget build(BuildContext c, MyCounterViewModel vm) => ListTile(
        title: Text('測(cè)試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.初級(jí)使用方法',
        home: Scaffold(
          appBar: AppBar(),
          body: Column(children: <Widget>[
            // 將視圖放入需要的地方
            MyCounterView(),
          ]),
        ),
      );
}

3-在main方法中注冊(cè)依賴

這里還是沿用 Demo0中的方法

包裝View的Demo到此結(jié)束, 這樣的寫法適用于Model十分簡(jiǎn)單的情況, 但實(shí)際上如果Model十分簡(jiǎn)單, 也就失去使用狀態(tài)管理的意義了, 圖一樂也就圖一樂,真圖一樂還得看Demo3

</br></br></br>


</br>

?? 自定義 Model (Demo2)

在實(shí)際應(yīng)用中, Model肯定不會(huì)是一個(gè)基本類型, 否則也就失去使用狀態(tài)管理的意義了

? 建議自己動(dòng)手的時(shí)候也按照本文中的步驟操作

</br>

0-先確保配置了yaml

dependencies:
  flutter:
    sdk: flutter
  ## 1. 引入get_state
  get_state: ^3.3.0

  ## 2- 可以通過引入equatable,省去手動(dòng)覆寫==和hashCode
  equatable: ^1.1.1

</br>

1-編寫Model(CounterModel)

建立一個(gè)簡(jiǎn)單的狀態(tài), 內(nèi)部有兩個(gè)變量 number和str
Model有兩種寫法, 其實(shí)本質(zhì)上沒有區(qū)別, 先看看寫法1

/// 寫法1
class CounterModel {
  final int number;
  final String str;

  CounterModel(this.number, this.str);

  // todo 注意, 這里務(wù)必覆寫==與hashCode, 否則無(wú)法正常刷新
  @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貫徹"解放雙手,保護(hù)頭發(fā)"的理念.

雖然有IDE加持, 覆寫==與hashCode方法并一般不費(fèi)時(shí)間.
但如果Model中的字段很多,頻繁修改字段的同時(shí), 還要修改 ==與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都不用動(dòng)手了
  @override
  final stringify = true;
}

</br>

2-編寫ViewModel(CounterVm)

這里沿用Demo0中的代碼

</br>

3-編寫View(MyCounterView)

這里沿用Demo1中的View

</br>

4-再將View放入Widget樹

仍然沿用Demo1中的代碼

</br>

5-最后不要忘記注冊(cè)依賴(自動(dòng)注冊(cè)就不用考慮這一步了)

還是用Demo0中的依賴注冊(cè)方式

</br>

GetState基礎(chǔ)使用教程至此結(jié)束, 是不是十分簡(jiǎn)單呢? ??

</br></br>


</br>

?? 半自動(dòng)注冊(cè)狀態(tài)與跨頁(yè)狀態(tài)修改 (Demo3)

?? emmm, 不用多說, 肯定有全自動(dòng)注冊(cè)的方法了,
不過由于篇幅有限, 全自動(dòng)注冊(cè)的方法請(qǐng)參考 這里, 這里不再做詳細(xì)說明(不建議新手使用)

</br>

0-先確保配置了yaml

? 這里的yaml與之前的相差較大, 注意觀察

dependencies:
  flutter:
    sdk: flutter
  ## 1. 引入get_state
  get_state: ^3.3.0

  ## 2- 可以通過引入equatable,省去手動(dòng)覆寫==和hashCode
  equatable: ^1.2.0
  
  ## 3- 通過injectable省去手動(dòng)注冊(cè)步驟
  injectable: ^0.4.0+1

dev_dependencies:
  flutter_test:
    sdk: flutter
  ## 4- injectable需要額外添加下面兩個(gè)依賴
  build_runner: ^1.10.0
  ## 5- 這個(gè)同樣重要
  injectable_generator: ^0.4.1

</br>

1-1頁(yè)面A-創(chuàng)建Model(CounterModel2)

本Demo將會(huì)創(chuàng)建兩個(gè)Page, 先看第一個(gè)頁(yè)面.
Model內(nèi)容與上一個(gè)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];
}

</br>

1-2頁(yè)面A-創(chuàng)建ViewModel(MyCounterViewModel)

?? 這里要注意, 一定要添加"@lazySingleton"注解, 這就是"半自動(dòng)"的一部分, 千萬(wàn)不要省略

不是光加上注解的完事了, "半自動(dòng)"還有另一半操作呢??

@lazySingleton
class MyCounterViewModel extends ViewModel<CounterModel2> {
  MyCounterViewModel() : super(initModel: CounterModel2(3, '- -'));

  int get counter => m.number;

  void incrementCounter() {
    vmUpdate(CounterModel2(m.number + 1, '新的值'));
  }
}

</br>

1-3頁(yè)面A-創(chuàng)建View(MyCounterView)

class MyCounterView extends View<MyCounterViewModel> {
  @override
  Widget build(BuildContext c, MyCounterViewModel vm) => ListTile(
        leading: Text('測(cè)試3: ${vm.counter}'),
        title: Text('${vm.m.str}'),
        trailing: RaisedButton(
          child: Icon(Icons.add),
          onPressed: () => vm.incrementCounter(),
        ),
      );
}

</br>

1-4頁(yè)面A-將View放到Page中

這里的MapApp 跟前面的不太一樣, 不要太在意這些細(xì)節(jié), 問題不大

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(
          title: Text('演示:3.標(biāo)準(zhǔn)使用方法'),
        ),
        body: Column(children: <Widget>[
          // View 1
          MyCounterView(),
          RaisedButton(
            child: Text('跳轉(zhuǎn)到新頁(yè)面'),
            onPressed: () => Navigator.of(context).push(MaterialPageRoute(
              builder: (c) => Page2(),
            )),
          ),
          RaisedButton(
            child: Text('點(diǎn)擊更改另一個(gè)頁(yè)面的值'),
            onPressed: () => g<Pg2Vm>().add,
          ),
        ]),
      );
}

看這里, "跨頁(yè)修改狀態(tài)"就是這么簡(jiǎn)單粗暴 ??

RaisedButton(
  child: Text('點(diǎn)擊更改另一個(gè)頁(yè)面的值'),
  onPressed: () => g<Pg2Vm>().add,
),

</br>

2-1頁(yè)面B-創(chuàng)建Model

頁(yè)面1的MVVM一家已經(jīng)創(chuàng)建完畢了, 頁(yè)面2只是為了演示跨頁(yè)狀態(tài)的修改, 所以就隨便寫一下

// 你沒看錯(cuò), 頁(yè)面2不定義Model了, 直接用int類型吧

</br>

2-2頁(yè)面B-創(chuàng)建ViewModel(Pg2Vm)

跟上面一樣, 同樣不要忘記加上"@lazySingleton"

@lazySingleton
class Pg2Vm extends ViewModel<int> {
  Pg2Vm() : super(initModel: 3);

  String get strVal => "$m";

  get add => vmUpdate(m + 1);
}

2-3頁(yè)面B-創(chuàng)建View(FooView)

再創(chuàng)建一個(gè)簡(jiǎn)單的View, 包裝以下ViewModel

class FooView extends View<Pg2Vm> {
  @override
  Widget build(BuildContext c, Pg2Vm vm) => RaisedButton(
        child: Text('${vm.strVal}'),
        onPressed: () => vm.add,
      );
}

2-4頁(yè)面B-將View放入Page中

class Page2 extends StatelessWidget{
  @override
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(),
        body: Center(
          child: FooView(),
        ),
      );
}

</br>

3-1初始化Injectable

將下面的函數(shù)直接寫在main.dart文件里面, 當(dāng)然,另外創(chuàng)建一個(gè)新dart文件也可以, 問題不大.

函數(shù), 一定要放在類的外面, 放在類里面的叫方法.

同樣不要放了添加注解"@injectableInit".
建議直接復(fù)制下面的代碼到自己項(xiàng)目里

寫好之后, IDE會(huì)提示"找不到$initGetIt"函數(shù),
不要著急, 這個(gè)函數(shù)還沒有自動(dòng)生成呢

// 添加注解
@injectableInit
Future<void> configDi() async {
  $initGetIt(g);
}

? 注意,這里的 configDi方法返回值是 Future<void>,
但是函數(shù)體內(nèi)沒有await.
這是因?yàn)楫?dāng)前生成的依賴注入代碼都是同步的, 如果用到了@preResolve注解, 則生成的 $initGetIt()是一個(gè)異步方法, 必須要加上await,否則會(huì)出錯(cuò)

</br>

3-2自動(dòng)生成注入代碼

打開Terminal(或者用CMD進(jìn)入項(xiàng)目的lib同級(jí)路徑),
輸入

flutter pub run build_runner build --delete-conflicting-outputs

如果希望build_runner在后臺(tái)持續(xù)自動(dòng)生成代碼,則輸入

flutter pub run build_runner watch --delete-conflicting-outputs

這里的"--delete-conflicting-outputs"表示清除已經(jīng)生成過的代碼, 如果你之前已經(jīng)生成過代碼, 而第二次生成又不想重新開始, 則可以不加這個(gè)參數(shù)

如果生成失敗, 注意查看錯(cuò)誤代碼, 一般情況下加上"--delete-conflicting-outputs"就能解決問題

待代碼生成完畢后, 在原本報(bào)錯(cuò)的代碼處import新生成的 xxx.iconfig.dart文件就可以了.

</br>

4-在main中添加依賴注入

GetIt g = GetIt.instance;

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  // 5. 添加自動(dòng)依賴注入
  configDi();
  runApp(MaterialApp(home: MyApp()));
}

</br></br></br>


</br>

??????大功告成??????

</br>

以上Demo就是get_state的一般用法了, 不過除此之外, get_state還有更多技巧等待你的解鎖??

下面幾個(gè)Demo的依賴于這個(gè)文件.dart, 直接復(fù)制粘貼是無(wú)法運(yùn)行的, 具體原因是因?yàn)闆]有為自己生成相應(yīng)的 依賴注入代碼

</br>
</br>

希望各位多多點(diǎn)贊支持, 更歡迎大家提出意見與建議??

有時(shí)間的話會(huì)補(bǔ)上后續(xù)教程的??

后續(xù)

  • 關(guān)于上文中留下的問題

"?? 猜一猜復(fù)雜的業(yè)務(wù)邏輯應(yīng)該怎么處理", 請(qǐng)參見GetArch介紹

</br>
新增

??

未經(jīng)作者授權(quán), 禁止轉(zhuǎn)載

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
禁止轉(zhuǎn)載腾么,如需轉(zhuǎn)載請(qǐng)通過簡(jiǎn)信或評(píng)論聯(lián)系作者醋虏。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市哮翘,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌毛秘,老刑警劉巖饭寺,帶你破解...
    沈念sama閱讀 218,451評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異叫挟,居然都是意外死亡艰匙,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門抹恳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)员凝,“玉大人,你說我怎么就攤上這事奋献〗∨” “怎么了?”我有些...
    開封第一講書人閱讀 164,782評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵瓶蚂,是天一觀的道長(zhǎng)糖埋。 經(jīng)常有香客問我,道長(zhǎng)窃这,這世上最難降的妖魔是什么瞳别? 我笑而不...
    開封第一講書人閱讀 58,709評(píng)論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮杭攻,結(jié)果婚禮上祟敛,老公的妹妹穿的比我還像新娘。我一直安慰自己兆解,他們只是感情好馆铁,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,733評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著锅睛,像睡著了一般叼架。 火紅的嫁衣襯著肌膚如雪畔裕。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,578評(píng)論 1 305
  • 那天乖订,我揣著相機(jī)與錄音扮饶,去河邊找鬼。 笑死乍构,一個(gè)胖子當(dāng)著我的面吹牛甜无,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播哥遮,決...
    沈念sama閱讀 40,320評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼岂丘,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了眠饮?” 一聲冷哼從身側(cè)響起奥帘,我...
    開封第一講書人閱讀 39,241評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤仪召,失蹤者是張志新(化名)和其女友劉穎寨蹋,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體扔茅,經(jīng)...
    沈念sama閱讀 45,686評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡已旧,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,878評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了召娜。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片运褪。...
    茶點(diǎn)故事閱讀 39,992評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖玖瘸,靈堂內(nèi)的尸體忽然破棺而出秸讹,到底是詐尸還是另有隱情,我是刑警寧澤雅倒,帶...
    沈念sama閱讀 35,715評(píng)論 5 346
  • 正文 年R本政府宣布嗦枢,位于F島的核電站,受9級(jí)特大地震影響屯断,放射性物質(zhì)發(fā)生泄漏文虏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,336評(píng)論 3 330
  • 文/蒙蒙 一殖演、第九天 我趴在偏房一處隱蔽的房頂上張望氧秘。 院中可真熱鬧,春花似錦趴久、人聲如沸丸相。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)灭忠。三九已至膳算,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間弛作,已是汗流浹背涕蜂。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留映琳,地道東北人机隙。 一個(gè)月前我還...
    沈念sama閱讀 48,173評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像萨西,于是被迫代替她去往敵國(guó)和親有鹿。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,947評(píng)論 2 355