Flutter | 狀態(tài)管理特別篇——Provide

前言

今天偶然發(fā)現(xiàn)在谷歌爸爸的倉庫下出現(xiàn)了一個叫做flutter-provide的狀態(tài)管理框架,2月8日才第一次提交,非常新鮮。在簡單上手之后感覺就是一個字——爽!所以今天就跟大家分享一下這個新的狀態(tài)管理框架。

Provider被設(shè)計為ScopedModel的替代品,并且允許我們更加靈活地處理數(shù)據(jù)類型和數(shù)據(jù)。但是首先呢還是先說說老生常談的狀態(tài)管理。

為什么需要狀態(tài)管理

在我們一開始構(gòu)建應(yīng)用的時候,也許很簡單。我們有一些狀態(tài),直接把他們映射成視圖就可以了。這種簡單應(yīng)用可能并不需要狀態(tài)管理。

image

但是隨著功能的增加,你的應(yīng)用程序?qū)袔资畟€甚至上百個狀態(tài)。這個時候你的應(yīng)用應(yīng)該會是這樣。

image

Wow,這是什么鬼。我們很難再清楚的測試維護我們的狀態(tài),因為它看上去實在是太復(fù)雜了!而且還會有多個頁面共享同一個狀態(tài),例如當(dāng)你進入一個文章點贊,退出到外部縮略展示的時候,外部也需要顯示點贊數(shù),這時候就需要同步這兩個狀態(tài)。

這時候,我們便迫切的需要一個架構(gòu)來幫助我們理清這些關(guān)系,狀態(tài)管理框架應(yīng)運而生。

什么是Provide

和Scoped_model一樣,Provide也是借助了InheritWidget,將共享狀態(tài)放到頂層MaterialApp之上。底層部件通過Provier獲取該狀態(tài),并通過混合ChangeNotifier通知依賴于該狀態(tài)的組件刷新。

Provide還提供了Provide.stream,讓我們能夠以處理流的方式處理數(shù)據(jù),不過目前還有一些問題,不推薦使用。

Lets do it!

我們這里還是以一個簡單app為例,詳細(xì)介紹Provide的用法。其中涉及共享狀態(tài)以及多個狀態(tài)之間如何管理。

image

這兩個頁面都同時依賴于counter 和 switcher兩個不同的狀態(tài)。并且一個頁面改變狀態(tài)之后另外一個頁面狀態(tài)也隨之改變。

該項目完整代碼已放在 Github

第一步:添加依賴

在pubspec.yaml中添加Provide的依賴。


image

第二步:創(chuàng)建Model

這里實際上它承擔(dān)了State的職責(zé),但是為了和官方的State區(qū)分所以叫做model。

import 'package:flutter/material.dart';

class Counter with ChangeNotifier{
  int value = 0;
  
  increment(){
    value++;
    notifyListeners();
  }
}

這里我們可以看到,數(shù)據(jù)和操作數(shù)據(jù)的方法都在model中,我們可以很清晰的把業(yè)務(wù)分離出來。

對比Scoped_model可以發(fā)現(xiàn),Provide模式中model不再需要繼承Model類,只需要實現(xiàn)Listenable,我們這里混入ChangeNotifier,可以不用管理聽眾。

通過 notifyListeners 我們可以通知聽眾刷新。

第三步:將狀態(tài)放入頂層

void main() {
  var counter = Counter();
  var providers = Providers();

//將counter對象添加進providers
  providers.provide(Provider<Counter>.value(counter));

  runApp(
    ProviderNode(
        child: MyApp(), 
        providers: providers),
    );
}

ProviderNode封裝了InheritWidget,并且提供了
一個providers容器用于放置狀態(tài)。

ProviderScope 為Provider提供單獨的類型空間,它允許多個相同類型的提供者。默認(rèn)使用ProviderScope('_default'),存放的時候你可以通過ProviderScope("name")來指定key。

添加一組Provider的時候建議使用provideFrom或者provide方法,而不是provideAll,因為它可以檢查編譯時的類型錯誤。

Provider<Counter>.value將counter包裝成了_ValueProvider。并在它的內(nèi)部提供了StreamController從而實現(xiàn)對數(shù)據(jù)進行流式操作。

image

第四步:獲取狀態(tài)

同樣的Provide也提供了兩種獲取State的方法。我們先來介紹第一種,通過Provide<T>小部件獲取。

Provide<Counter>(
              builder: (context, child, counter) {
                return Text(
                  '${counter.value}',
                  style: Theme.of(context).textTheme.display1,
                );
              },
            ),

每次通知數(shù)據(jù)刷新時,builder將會重新構(gòu)建這個小部件。

builder方法接收三個參數(shù),這里主要介紹第二個和第三個。

  • 第二個參數(shù)child:假如這個小部件足夠復(fù)雜,內(nèi)部有一些小部件是不會改變的,那么我們可以將這部分小部件寫在Provide的child屬性中,讓builder不再重復(fù)創(chuàng)建這些小部件,以提升性能。
  • 第三個參數(shù)counter:這個參數(shù)代表了我們獲取的頂層providers中的狀態(tài)<T>。

scope:通過指定ProviderScope獲取該鍵所對應(yīng)的狀態(tài)。在需要使用多個相同類型狀態(tài)的時候使用。

第二種獲取方式:Provide.value<T>(context)

final currentCounter = Provide.value<Counter>(context);

這種方式實際上調(diào)用了context.inheritFromWidgetOfExactType找到頂層的_InheritedProviders來獲取到頂層providers中的狀態(tài)。

如何組織多個狀態(tài)

和scoped_model不同的是,provide模式中你可以輕松組織多個狀態(tài)。只需要將狀態(tài)provide進provider中就可以了。

void main() {
  var counter = Counter();
  var switcher = Switcher();

  var providers = Providers();

  providers
    ..provide(Provider<Counter>.value(counter))
    ..provide(Provider<Switcher>.value(switcher));

  runApp(
    ProviderNode(
        child: MyApp(), 
        providers: providers)
    );
}

獲取數(shù)據(jù)流

在將counter添加進providers的過程中進行了一次包裝。我們剛才通過分析源碼知道了這個操作能夠讓我們處理流式數(shù)據(jù)。

通過 Provide.stream<T>(context) 就能獲取數(shù)據(jù)流。需要注意的是,這里每次獲取的數(shù)據(jù)流都

StreamBuilder<Counter>(
          initialData: currentCounter,
          stream: Provide.stream<Counter>(context)
              .where((counter) => counter.value % 2 == 0),
          builder: (context, snapshot) =>
              Text('Last even value: ${snapshot.data.value}')),

不過在我的使用當(dāng)中出現(xiàn)了streamTransformer失效的情況。在firstScreen和secondScreen同樣應(yīng)用這一段相同的代碼,second screen的where方法能夠生效,過濾掉奇數(shù)數(shù)據(jù),而first screen中則是收到了完整的數(shù)據(jù)。

需要注意的是,這里每次獲取的數(shù)據(jù)流都會重新創(chuàng)建一條新的流。

  /// Creates a provider that listens to a stream and caches the last
  /// received value of the stream.
  /// This provider notifies for rebuild after every release.
  factory Provider.stream(Stream<T> stream, {T initialValue}) =>
      _StreamProvider<T>(stream, initialValue: initialValue);

關(guān)于這個做法還有一些爭議,具體可以查看這個issue:
https://github.com/google/flutter-provide/issues/3

不過這個功能還可以結(jié)合rxdart使用,可以通過stream輕松構(gòu)建Observer,讓我們更加靈活的組織數(shù)據(jù)。

根據(jù)多個狀態(tài)重建小部件

當(dāng)我們一個視圖可能依賴于多個狀態(tài)進行重建的時候,可以使用ProvideMulti小部件。

已知的坑

Provide.stream 詭異的手動監(jiān)聽

由于 Provide 自動將 Listenable 數(shù)據(jù)包裝并提供了 Provide.stream 接口,讓我們可以通過監(jiān)聽這個流,來獲取最新事件。但是當(dāng)我們進行手動監(jiān)聽之后將會發(fā)生這件詭異的事情。

···
class _SecondScreenState extends State<SecondScreen> {
  StreamSubscription<Switcher> _subscription;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _subscription = Provide.stream<Switcher>(context).listen((data){
      print(data.toString());
    });
  }

  @override
  void dispose() {
    unSubscribe();
    super.dispose();
  }

  unSubscribe(){
    if(_subscription != null){
      _subscription.cancel();
      _subscription = null;
    }
  }
  ···

按理說這里應(yīng)該在數(shù)據(jù)發(fā)生變化的時候收到一條事件,可是我們這里發(fā)現(xiàn)一次性輸出了 5 條 flutter: Instance of 'Switcher'

為什么是 5 條呢,這是因為我一共在 5 處 地方收聽過這個數(shù)據(jù),包括使用 Provide Widget 也算一次收聽。

而當(dāng)我退出第二個頁面之后再次進入,發(fā)現(xiàn)這次收到的數(shù)據(jù)比上次多了 5 條。

出現(xiàn)這個現(xiàn)象是由于這個 stream 是由工廠方法創(chuàng)建,每次調(diào)用 Provide.stream 都會重新創(chuàng)建出來一條流。就算收聽者不再收聽,這條流也會存在。

所以不要去手動監(jiān)聽你的 Provide.stream。

寫在最后

自從上次寫完狀態(tài)管理拓展篇Rxdart之后斷更了三個月。總結(jié)篇遲遲沒有出來,在這里先說一聲抱歉。對于我來講狀態(tài)管理這個本身就是一個新鮮玩意,所以在沒有經(jīng)過大型應(yīng)用實戰(zhàn)檢驗的總結(jié)都是空談。
這也是為什么我遲遲沒有開始寫總結(jié)篇的原因。不過在這我可以說一些自己的感受,供大家參考。

在這幾個月中,我用的比較多的是BLoC,它組織數(shù)據(jù)確實非常靈活,可以很輕松的實現(xiàn)懶加載之類的操作。而且stateful widget寫的是越來越少了。缺點就是入門的門檻比較高,理解StreamTransformer和為什么需要pipe花了我不少時間。使用bloc思維方式需要比較大的改變,我看到了許多人在項目中使用bloc,但是用得很奇怪,還在以之前的思維模式思考。而且bloc只是對數(shù)據(jù)進行組織,共享狀態(tài)平時還是使用的InheritWidget,確實要做很多額外的功夫。

其次我比較喜歡的就是scoped_model,理由就是簡單好用。學(xué)習(xí)成本很低,而且沒有寫什么模版代碼。

我最不想使用的狀態(tài)管理方式就是redux了,一個是入門難度比較高,而且對于異步數(shù)據(jù)處理我也覺得是相當(dāng)麻煩的。但是閑魚團隊倒是喜歡redux,之后還會開源閑魚的狀態(tài)管理框架fish_redux。所以說,可能還是我編寫的應(yīng)用還不夠復(fù)雜,才會有這種感受。redux在復(fù)雜應(yīng)用上能夠更加清楚的劃分職責(zé),并且單向數(shù)據(jù)流以及state是immutable的特點這些都是redux的好處。

最后我再談?wù)凱rovide。Provide整體上給我的體驗非常接近Scoped,簡單易上手,并且更加強大。model不用再繼承,只用實現(xiàn)Listenable讓它不再具有侵入性。于此同時又增加了stream的特性,和bloc的做法又有幾分相似。如果你使用過Scoped_model你會很快就上手。

不過可以說的是,Provide是一個非常優(yōu)秀的狀態(tài)管理方式,值得你去使用。但是目前該package還存在一些問題,例如Provide.stream,在未來可能會進行較大的變動,需要慎重使用。

本次代碼已上傳Github: https://github.com/OpenFlutter/Flutter-Notebook/tree/master/mecury_project/example/flutter_provide

如果您對Provide還有任何疑問或者文章的建議,歡迎在下方評論區(qū)以及我的郵箱1652219550a@gmail.com與我聯(lián)系,我會及時回復(fù)!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,755評論 2 371

推薦閱讀更多精彩內(nèi)容