3.Flutte3.0 遙遙領(lǐng)先系列|一文教你完全掌握狀態(tài)管理, 手寫簡單版provider

重中之重, 源碼解析: (state的demo和源碼分析重點看看)

目錄

  1. StatelessWidget 與 StatefulWidget 區(qū)別? build方法
  2. 原始setstate的原理,狀態(tài)管理的重要性 (重點)
  3. 官方 provider的原理解析 (重點)
    3.1重點的2種方式和源碼分析 , (provider和InheritedWidget)
    3.2Provider的案例使用ChangeNotifier, ChangeNotifierProvider 實現(xiàn)2個組件之間的通信
    3.3Consumer 刷新指定區(qū)域
    3.4 Selector的用法
    3.5MultiProvider 多zhuang狀態(tài)共享.
    事實上力喷,當我們使用 Provider 后莉掂,我們就再也不需要使用 StatefulWidget 了。
  4. 手寫簡單版provider
  5. 基礎(chǔ)的跨組件框架, InheritedWidget、Notification和EventBus
  6. 第三方框架圖文比較, 咸魚Fish Redux的框架 , getX的狀態(tài)管理

狀態(tài)管理是什么:

jietu-1706527334684.jpg

Flutter的狀態(tài)可以分為全局狀態(tài)和局部狀態(tài)兩種指蚁。

Flutter 狀態(tài)管理是指在 Flutter 應(yīng)用中有效地管理應(yīng)用的數(shù)據(jù)和狀態(tài)

狀態(tài)管理是聲明式編程非常重要的一個概念

問題: 為什么要做狀態(tài)管理?

就是有幾個頁面, 要實現(xiàn)數(shù)據(jù)的同步或者共享!

下面是官方給出的一些原則可以幫助你做決定:

  • 如果狀態(tài)是用戶數(shù)據(jù)厕九,如復選框的選中狀態(tài)、滑塊的位置除师,則該狀態(tài)最好由父 Widget 管理沛膳。
  • 如果狀態(tài)是有關(guān)界面外觀效果的,例如顏色汛聚、動畫锹安,那么狀態(tài)最好由 Widget 本身來管理。
  • 如果某一個狀態(tài)是不同 Widget 共享的則最好由它們共同的父 Widget 管理。

1. StatelessWidget 與 StatefulWidget 區(qū)別?

StatelessWidget和StatefulWidget的區(qū)別就在這個可變的State了叹哭。

當狀態(tài)數(shù)據(jù)發(fā)生變化時忍宋,F(xiàn)lutter 會調(diào)用 build() 方法重新構(gòu)建界面

問題: build方法什么情況下被執(zhí)行呢?:

  • 1)风罩、當我們的StatelessWidget第一次被插入到Widget樹中時(也就是第一次被創(chuàng)建時)糠排;
  • 2)、當我們的父Widget(parent widget)發(fā)生改變時超升,子Widget會被重新構(gòu)建入宦;
  • 3)、如果我們的Widget依賴InheritedWidget的一些數(shù)據(jù)室琢,InheritedWidget數(shù)據(jù)發(fā)生改變時云石;

Stateful widget特有:

至少由兩個類組成:

 一個StatefulWidget類。

 一個 State類研乒; StatefulWidget類本身是不變的汹忠,但是State類中持有的狀態(tài)在 widget 生命周期中可能會發(fā)生變化。

問題: 為什么要將 build 方法放在 State 中雹熬,而不是放在StatefulWidget中宽菜?

1).狀態(tài)訪問不便, 屬性會被公開

試想一下,如果我們的StatefulWidget有很多狀態(tài)竿报,而每次狀態(tài)改變都要調(diào)用build方法铅乡,由于狀態(tài)是保存在 State 中的,如果build方法在StatefulWidget中烈菌,那么build方法和狀態(tài)分別在兩個類中阵幸,那么構(gòu)建時讀取狀態(tài)將會很不方便!

試想一下芽世,如果真的將build方法放在 StatefulWidget 中的話挚赊,由于構(gòu)建用戶界面過程需要依賴 State,所以build方法將必須加一個State參數(shù)济瓢,大概是下面這樣:

  Widget build(BuildContext context, State state){
      //state.counter
      ...
  }

這樣的話就只能將State的所有狀態(tài)聲明為公開的狀態(tài)荠割,這樣才能在State類外部訪問狀態(tài)!但是旺矾,將狀態(tài)設(shè)置為公開后蔑鹦,狀態(tài)將不再具有私密性,這就會導致對狀態(tài)的修改將會變的不可控箕宙。但如果將build()方法放在State中的話嚎朽,構(gòu)建過程不僅可以直接訪問狀態(tài),而且也無需公開私有狀態(tài)柬帕,這會非常方便哟忍。

2.繼承StatefulWidget不便室囊。

例如,F(xiàn)lutter 中有一個動畫 widget 的基類AnimatedWidget魁索,它繼承自StatefulWidget類融撞。AnimatedWidget中引入了一個抽象方法build(BuildContext context),繼承自AnimatedWidget的動畫 widget 都要實現(xiàn)這個build方法〈治担現(xiàn)在設(shè)想一下尝偎,如果StatefulWidget 類中已經(jīng)有了一個build方法,正如上面所述鹏控,此時build方法需要接收一個 State 對象致扯,這就意味著AnimatedWidget必須將自己的 State 對象(記為_animatedWidgetState)提供給其子類,因為子類需要在其build方法中調(diào)用父類的build方法当辐,代碼可能如下:

問題: 為什么flutter在設(shè)計的時候statefulWidget的build方法放在state中?

  1. build依賴state中的變量
    2.widget會不停的銷毀
  2. 專題改變, 不希望把state改變

問題: build的context是什么

在StatelessElement中抖僵,我們發(fā)現(xiàn)是將this傳入,所以本質(zhì)上BuildContext就是當前的Element
context是當前 widget 在 widget 樹中位置中執(zhí)行”相關(guān)操作“的一個句柄(handle)缘揪,比如它提供了從當前 widget 開始向上遍歷 widget 樹以及按照 widget 類型查找父級 widget 的方法

class ContextRoute extends StatelessWidget  {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Context測試"),
      ),
      body: Container(
        child: Builder(builder: (context) {
          // 在 widget 樹中向上查找最近的父級`Scaffold`  widget 
          Scaffold scaffold = context.findAncestorWidgetOfExactType<Scaffold>();
          // 直接返回 AppBar的title耍群, 此處實際上是Text("Context測試")
          return (scaffold.appBar as AppBar).title;
        }),
      ),
    );
 // 查找父級最近的Scaffold對應(yīng)的ScaffoldState對象
                  ScaffoldState _state = context.findAncestorStateOfType<ScaffoldState>()!;

如果 StatefulWidget 的狀態(tài)是希望暴露出的,應(yīng)當在 StatefulWidget 中提供一個of 靜態(tài)方法來獲取其 State 對象找筝,開發(fā)者便可直接通過該方法來獲鹊腹浮;如果 State不希望暴露袖裕,則不提供of方法

  // 直接通過of靜態(tài)方法來獲取ScaffoldState
      ScaffoldState _state=Scaffold.of(context);

StatelessWidget特有
問題: 我之前說過定義到Widget中的數(shù)據(jù)都是不可變的曹抬,必須定義為final,為什么呢急鳄?
Flutter如何做到我們在開發(fā)中定義到Widget中的數(shù)據(jù)一定是final的呢谤民?

@immutable
abstract class Widget extends DiagnosticableTree {
    // ...省略代碼
}

這里有一個很關(guān)鍵的東西@immutable

  • 我們似乎在Dart中沒有見過這種語法,這實際上是一個 注解疾宏,這設(shè)計到Dart的元編程张足,我們這里不展開講;

  • 來源: https://api.flutter.dev/flutt...

  • 說明: 被@immutable注解標明的類或者子類都必須是不可變的

2. 原始setstate的原理,狀態(tài)管理的重要性

源碼分析:

setState 僅在本地范圍內(nèi)有效灾锯,如果一個 Widget 需要改變它自己的狀態(tài)兢榨,那么 setState 就是你最好的選擇

setstate() 主要用于修改數(shù)據(jù),變量值的! 相當于notifychange

問題: 如何從statefullWidget把數(shù)據(jù)傳遞到state類中?

可以2次傳遞

還有一種方法,state中可以直接取到statefullWidget的實例_widget , 就可以!

之前最簡單的就是 widget, setstate

那 State 是在哪里被創(chuàng)建的?

class StatefulElement extends ComponentElement {
  StatefulElement(StatefulWidget widget)
      : _state = widget.createState(),
        super(widget) {
    assert(() {
      if (!state._debugTypesAreRight(widget)) {
        throw FlutterError.fromParts(<DiagnosticsNode>[
          ErrorSummary('StatefulWidget.createState must return a subtype of State<${widget.runtimeType}>'),
          ErrorDescription(
            'The createState function for ${widget.runtimeType} returned a state '
            'of type ${state.runtimeType}, which is not a subtype of '
            'State<${widget.runtimeType}>, violating the contract for createState.',
          ),
        ]);
      }
      return true;
    }());
    assert(state._element == null);
    state._element = this;
    state._widget = widget;
    assert(state._debugLifecycleState == _StateLifecycle.created);
  }

更新ui, state是啥?

  @override
  void update(ProxyWidget newWidget) {
    final ProxyWidget oldWidget = widget as ProxyWidget;
    assert(widget != newWidget);
    super.update(newWidget);
    assert(widget == newWidget);
    updated(oldWidget);
    rebuild(force: true);
  }

調(diào)用widget的setstate方法沒, 會執(zhí)行StatefulElement的update()方法

  @protected
  void setState(VoidCallback fn) {
    assert(() {
    final Object? result = fn() as dynamic;
    }());
    _element!.markNeedsBuild();  // markNeedsBuild () 
  }

 void markNeedsBuild() {
    assert(_lifecycleState != _ElementLifecycle.defunct);
    if (_lifecycleState != _ElementLifecycle.active) {
      return;
    }
    assert(owner != null);
    assert(_lifecycleState == _ElementLifecycle.active);
    assert(() {
      if (owner!._debugBuilding) {
        assert(owner!._debugCurrentBuildTarget != null);
        assert(owner!._debugStateLocked);
        if (_debugIsInScope(owner!._debugCurrentBuildTarget!)) {
          return true;
        }
        final List<DiagnosticsNode> information = <DiagnosticsNode>[
          ErrorSummary('setState() or markNeedsBuild() called during build.'),
          ErrorDescription(
          ),
          describeElement('The widget on which setState() or markNeedsBuild() was called was'),
        ];
      return true;
    }());
    if (dirty) { // 這里進行了返回! 
      return;
    }
    _dirty = true;
    owner!.scheduleBuildFor(this);  // 調(diào)用scheduleBuildFor方法, 傳入當前Element對象
  }
  void scheduleBuildFor(Element element) {
    assert(element.owner == this);
    assert(() {
      if (debugPrintScheduleBuildForStacks) {
        debugPrintStack(label: 'scheduleBuildFor() called for $element${_dirtyElements.contains(element) ? " (ALREADY IN LIST)" : ""}');
      }
      return true;
    }());
    if (element._inDirtyList) {
      _dirtyElementsNeedsResorting = true;
      return;
    }
    if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
      _scheduledFlushDirtyElements = true;
      onBuildScheduled!();
    }
    _dirtyElements.add(element);  //添加到_dirtyElements集合中
    element._inDirtyList = true;
      return true;
    }());
  }

從現(xiàn)象推斷顺饮,整個流程必然會經(jīng)過setState()-···················->當前State的build()-················->頁面繪制-············->屏幕刷新
問題: setState ()是如何更新UI的?
setState源碼分析總結(jié):

我們常說的 setState ,其實是調(diào)用了 markNeedsBuild 凌那,markNeedsBuild 內(nèi)部會標記 element 為 diry兼雄,添加到BuildOwner對象的_dirtyElements集合中, 然后調(diào)用scheduleFrame來注冊Vsync回調(diào)。 當下一次vsync信號的到來時會執(zhí)行handleBeginFrame()和handleDrawFrame()來更新UI帽蝶。
然后在下一幀 WidgetsBinding.drawFrame 才會被繪制赦肋,這可以也看出 setState 并不是立即生效的

State#setState 的核心作用就是把持有的元素標臟并申請新幀調(diào)度。而只有新幀到來,執(zhí)行完構(gòu)建之后佃乘,元素的 dirty 才會置為 false 囱井。也就是說,兩幀之間趣避,無論調(diào)用多少次 setState 庞呕,都只會觸發(fā)一次程帕, 元素標臟 和 申請新幀調(diào)度 。這就是為什么連續(xù)觸發(fā) 1000000 次愁拭,并無大事發(fā)生的原因

setState()會重建, 但是有dirty的判讀, 不會經(jīng)常重建!

dirty state的含義是臟的State
它實際是通過一個Element的東西(我們還沒有講到Flutter繪制原理)的屬性來標記的;
將它標記為dirty會等待下一次的重繪檢查盏混,強制調(diào)用build方法來構(gòu)建我們的Widget
setState可以分為兩個部分:

將element標臟
渲染時將所有臟element都rebuild惜论,且將自己的child進行update
重要的方法: performRebuild()

updateChild(_child, built, slot)

問題: setState每次都會去執(zhí)行build ()?

父widget
父widget2
子widget3: 用state變量

點擊: 調(diào)用setstate方法, 然后會創(chuàng)建, 調(diào)用 build(). 是不是只重繪制子widget3, 其他的不會重新繪制
setState 觸發(fā)了對你當前所在的小組件的重建。如果你的整個應(yīng)用程序只包含一個widget来涨,那么整個widget將被重建,這將使你的應(yīng)用程序變得緩慢
要把setstate多封裝一層, 讓setstate咋你自己的widget, 這樣就不會重建整個widget

問題: 為什么高位置的setState ()會消耗性能?

雖然setState的調(diào)用并沒有像 Widget 層那樣技羔,在渲染控制層的 Element 那一層重新構(gòu)建全部element。但是藤滥,這并不代表 setState 的使用沒問題社裆,首先拙绊,像之前篇章說的那樣,它會重新構(gòu)建整個 Widget 樹泳秀,這會帶來性能損耗标沪;其次,由于整個 Widget 樹改變了嗜傅,意味著整棵樹對應(yīng)的渲染層Element對象都會執(zhí)行 update方法金句,雖然不一定會重新渲染,但是這整棵樹的遍歷的性能開銷也很高

總結(jié): 雖然每次不會都創(chuàng)建element, 但是遍歷有損耗, 提升辦法: 只更新需要更新的widget!
當我們在一個高節(jié)點調(diào)用setState()的時候會構(gòu)建再次build所有的Widget吕嘀,雖然不一定掛載到Element樹中违寞,但是平時我們使用的Widget中往往嵌套多個其他類型的Widget贞瞒,每個build()方法走下來最終也會帶來不小的開銷,因此通過各種狀態(tài)管理方案趁曼,Stream等方式军浆,只做局部刷新,是我們?nèi)粘i_發(fā)中應(yīng)該養(yǎng)成的良好習慣挡闰。

流轉(zhuǎn)圖:
setState() 添加到_dirtyElements集合中, owner.scheduleBuildFor(this)
rebuild()
StatefulElement-->performRebuild(): 最重要的方法
StatefulElement--->updateChild()
Element---> update();

build()過程雖然只是調(diào)用一個組件的構(gòu)造方法乒融,不涉及對Element樹的掛載操作。

但因為我們一個組件往往是N多個Widget的嵌套組合尿这,每個都遍歷一遍開銷算下來并不小

問題: setState()如何做優(yōu)化?

要在子控件中調(diào)用setState(),

局部刷新 , 但是局部刷新簇抵,也只是 setState 的封裝

setstate.jpg

3. 官方 provider: 復雜情況的狀態(tài)管理

**數(shù)據(jù)傳遞: 多個頁面之間的傳遞, 數(shù)據(jù)變化通知! **

Flutter 官方的狀態(tài)管理框架 Provider 則相對簡單得多

Provider 是一個用來提供數(shù)據(jù)的框架。它是 InheritedWidget 的語法糖射众,提供了依賴注入的功能碟摆,允許在 Widget 樹中更加靈活地處理和傳遞數(shù)據(jù)

Provider 就是針對 InheritedWidget 的一個包裝工具

在使用Provider的時候,我們主要關(guān)心三個概念:

  • ChangeNotifier:真正數(shù)據(jù)(狀態(tài))存放的地方
  • ChangeNotifierProvider:Widget樹中提供數(shù)據(jù)(狀態(tài))的地方叨橱,會在其中創(chuàng)建對應(yīng)的ChangeNotifier
  • Consumer:Widget樹中需要使用數(shù)據(jù)(狀態(tài))的地方 讀Provider

讀和寫的Provider
ChangeNotifierProvider:Widget樹中提供數(shù)據(jù)(狀態(tài))的地方典蜕,會在其中創(chuàng)建對應(yīng)的ChangeNotifier
但是,濫用 Provider.of 方法也有副作用罗洗,那就是當數(shù)據(jù)更新時愉舔,頁面中其他的子 Widget 也會跟著一起刷新。
可以看到轩缤,TestIcon 控件本來是一個不需要刷新的 StatelessWidget火的,但卻因為其父 Widget FloatingActionButton 所依賴的數(shù)據(jù)資源 counter 發(fā)生了變化馏鹤,導致它也要跟著刷新湃累。

Consumer: 消費, 把數(shù)據(jù)消費! (用的比 Provider.of 多)

因為Consumer在刷新整個Widget樹時治力,會盡可能少的rebuild Widget琴许。
在數(shù)據(jù)資源發(fā)生變化時榜田,只刷新對資源存在依賴關(guān)系的 Widget箭券,而其他 Widget 保持不變呢?
有一個child().不重新構(gòu)建
Consumer是否是最好的選擇呢荆永?并不是豆村,它也會存在弊端 Selector的選擇 (使用的比較多)

比如當點擊了floatingActionButton時,我們在代碼的兩處分別打印它們的builder是否會重新調(diào)用薇正;
我們會發(fā)現(xiàn)只要點擊了floatingActionButton眷射,兩個位置都會被重新builder;
但是floatingActionButton的位置有重新build的必要嗎苦囱?沒有鱼鸠,因為它是否在操作數(shù)據(jù)蚀狰,并沒有展示麻蹋;

如何可以做到讓它不要重新build了扮授?使用Selector來代替Consumer

所以在某些情況下,我們可以使用Selector來代替Consumer堪侯,性能會更高
Selector和Consumer對比伍宦,不同之處主要是三個關(guān)鍵點:

關(guān)鍵點1:泛型參數(shù)是兩個
泛型參數(shù)一:我們這次要使用的Provider
泛型參數(shù)二:轉(zhuǎn)換之后的數(shù)據(jù)類型,比如我這里轉(zhuǎn)換之后依然是使用CounterProvider遇骑,那么他們兩個就是一樣的類型
關(guān)鍵點2:selector回調(diào)函數(shù)
轉(zhuǎn)換的回調(diào)函數(shù)势篡,你希望如何進行轉(zhuǎn)換
S Function(BuildContext, A) selector
我這里沒有進行轉(zhuǎn)換禁悠,所以直接將A實例返回即可
關(guān)鍵點3:是否希望重新rebuild
這里也是一個回調(diào)函數(shù)碍侦,我們可以拿到轉(zhuǎn)換前后的兩個實例瓷产;
bool Function(T previous, T next);
因為這里我不希望它重新rebuild濒旦,無論數(shù)據(jù)如何變化尔邓,所以這里我直接return false梯嗽;
多狀態(tài)的資源封裝

Provider 的另一個升級版 MultiProvider灯节,來實現(xiàn)多個 Provider 的組合注入
ScrollAwareImageProvider:
ChangeNotifier:
搞清楚TextField是怎么使用ChangeNotifier的了卡骂,為什么每次改變TextEditingController的text值偿警,然后在TextField數(shù)據(jù)框里的數(shù)據(jù)也及時改變了,其實最后還是用到setState盒使。
原理:
performRebuild() :該回調(diào)會在setState或者build的時候會觸發(fā)苞慢;此處做了一個判斷挽放,只會在第一次build的時候觸發(fā) build()
notifyDependent()

問題: InheritedElement? 而不是用普通的Element?

如Flutter SDK中正是通過InheritedWidget來共享應(yīng)用主題(Theme)和Locale (當前語言環(huán)境)信息

InheritedWidget: 作為根節(jié)點, 然后其他子節(jié)點, 就可以獲取根節(jié)點的狀態(tài)! 但是如果要更新的話, 還是的手動用setstate

最后辑畦,我們重寫了 updateShouldNotify 方法纯出,這個方法會在 Flutter 判斷 InheritedWidget 是否需要重建暂筝,從而通知下層觀察者組件更新數(shù)據(jù)時被調(diào)用到焕襟。在這里鸵赖,我們直接判斷 count 是否相等即可卫漫。

定義一個共享數(shù)據(jù)的InheritedWidget列赎,需要繼承自InheritedWidget

  • 這里定義了一個of方法包吝,該方法通過context開始去查找祖先的HYDataWidget(可以查看源碼查找過程)
  • updateShouldNotify方法是對比新舊HYDataWidget诗越,是否需要對更新相關(guān)依賴的Widget

InheritedWidget中的屬性在子Widget中只能讀嚷狞,如果有修改的場景床未,我們需要把它和StatefulWidget中的State配套使用薇搁。

InheritedWidget是Flutter中的一個功能型Widget啃洋,適用于在Widget樹中共享數(shù)據(jù)的場景宏娄。通過它绝编,我們可以高效地將數(shù)據(jù)在Widget樹中進行跨層傳遞

InheritedWidget使用方法

可以看到InheritedWidget的使用方法還是比較簡單的窟勃,無論Counter在CountContainer下層什么位置逗堵,都能獲取到其父Widget的計數(shù)屬性count蜒秤,再也不用手動傳遞屬性了作媚。

不過纸泡,InheritedWidget僅提供了數(shù)據(jù)讀的能力蚤假,如果我們想要修改它的數(shù)據(jù)磷仰,則需要把它和StatefulWidget中的State配套使用

使用場景:

InheritedWidget的數(shù)據(jù)流動方式是從父Widget到子Widget逐層傳遞

InheritedWidget共有兩個方法

1).createElement() (創(chuàng)建對應(yīng)的Element)

2).updateShouldNotify(covariant InheritedWidget oldWidget)

問題: Flutter中的InheritedWidget的實現(xiàn)原理是怎么樣的?
InheritedWidget的原理:

主要是觀察模式的思想

源碼分析: (重點)

class CountContainer extends InheritedWidget {
  // 方便其子 Widget 在 Widget 樹中找到它
  static CountContainer of(BuildContext context) => context.inheritFromWidgetOfExactType(CountContainer) as CountContainer;
  
  final int count;
 
  CountContainer({
    Key key,
    @required this.count,
    @required Widget child,
  }): super(key: key, child: child);
 
  // 判斷是否需要更新
  @override
  bool updateShouldNotify(CountContainer oldWidget) => count != oldWidget.count;
}

Provider 特點:
因為 Provider 實際上是 InheritedWidget 的語法糖伺通,所以通過 Provider 傳遞的數(shù)據(jù)從數(shù)據(jù)流動方向來看民逼,是由父到子(或者反過來)。這時我們就明白了疮鲫,原來需要把資源放到 FirstPage 和 SecondPage 的父 Widget弦叶,也就是應(yīng)用程序的實例 MyApp 中(當然伤哺,把資源放到更高的層級也是可以的,比如放到 main 函數(shù)中)

他最主要的功能:就是會調(diào)用Element的performRebuild()方法绢彤,然后觸發(fā)ComponentElement的build()方法,最終觸發(fā)_InheritedProviderScopeElement的build方法

其他: markNeedsNotifyDependents, 我們使用 notifyListeners()饶氏,就會觸發(fā)疹启,這個回調(diào)
provide不是調(diào)用的setstate()進行狀態(tài)管理的么?

那怎么會觸發(fā)到performRebuild()這個方法了?

當我們執(zhí)行 ChangeNotifer 的 notifyListeners 時喊崖,就會最終觸發(fā) setState 更新贷祈。
執(zhí)行流程呜达。
ChangeNotifer------>notifyListeners
setState()
InheritedElement------>performRebuild
InheritedElement------>build
InheritedElement------>notifyListeners

我們要知道一個前提:刷新Widget會先進入Element的rebuild方法查近。然后是performRebuild方法霜威,這個方法Element沒做什么,交由具體子類去實現(xiàn)
provider代碼原理總結(jié):

wiget是InheritedWidget

1)大猛、 Provider 的內(nèi)部 DelegateWidget 是一個 StatefulWidget 挽绩,所以可以更新且具有生命周期唉堪。
2)唠亚、狀態(tài)共享是使用了 InheritedProvider 這個 InheritedWidget 實現(xiàn)的趾撵。

4.手寫簡單版provider

使用
view

class CounterEasyPPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierEasyP(
      create: (BuildContext context) => CounterEasyP(),
      builder: (context) => _buildPage(context),
    );
  }

  Widget _buildPage(BuildContext context) {
    final easyP = EasyP.of<CounterEasyP>(context);

    return Scaffold(
      appBar: AppBar(title: Text('自定義狀態(tài)管理框架-EasyP范例')),
      body: Center(
        child: EasyPBuilder<CounterEasyP>(() {
          return Text(
            '點擊了 ${easyP.count} 次',
            style: TextStyle(fontSize: 30.0),
          );
        }),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => easyP.increment(),
        child: Icon(Icons.add),
      ),
    );

ChangeNotifier

class CounterEasyP extends ChangeNotifier {
  int count = 0;

  void increment() {
    count++;
    notifyListeners();
  }
}

核心類: 3個
ChangeNotifierProvider: 最重要的方法

class ChangeNotifierProvider<T extends ChangeNotifier> extends StatelessWidget {
  ChangeNotifierEasyP({
    Key? key,
    required this.create,
    this.builder,
    this.child,
  }) : super(key: key);

  final T Function(BuildContext context) create;

  final Widget Function(BuildContext context)? builder;
  final Widget? child;

@override
  Widget build(BuildContext context) {  // 重寫 build()方法, 返回InheritedWidget對象
    assert(
      builder != null || child != null,
      '$runtimeType  must specify a child',
    );

    return EasyPInherited(
      create: create,
      child: builder != null
          ? Builder(builder: (context) => builder!(context))
          : child!,
    );
  }
}
class EasyPInherited<T extends ChangeNotifier> extends InheritedWidget { // 實現(xiàn)一個InheritedWidget
  EasyPInherited({
    Key? key,
    required Widget child,
    required this.create,
  }) : super(key: key, child: child);

  final T Function(BuildContext context) create;

  @override
  bool updateShouldNotify(InheritedWidget oldWidget) => false;  // 重寫updateShouldNotify方法

  @override
  InheritedElement createElement() => EasyPInheritedElement(this);
}

class EasyPInheritedElement<T extends ChangeNotifier> extends InheritedElement { // InheritedElement重寫
  EasyPInheritedElement(EasyPInherited<T> widget) : super(widget);

  bool _firstBuild = true;
  bool _shouldNotify = false;
  late T _value;
  late void Function() _callBack;

  T get value => _value;

  @override
  void performRebuild() { // 實現(xiàn)performRebuild()
  if (_firstBuild) {
      _firstBuild = false;
      _value = (widget as EasyPInherited<T>).create(this);

      _value.addListener(_callBack = () {
        // 處理刷新邏輯究珊,此處無法直接調(diào)用notifyClients
        // 會導致owner!._debugCurrentBuildTarget為null剿涮,觸發(fā)斷言條件取试,無法向后執(zhí)行
        _shouldNotify = true;
        markNeedsBuild();
      });
    }

    super.performRebuild();
  }

  @override
  Widget build() {  // build()重寫
    if (_shouldNotify) {
      _shouldNotify = false;
      notifyClients(widget as EasyPInherited<T>);
    }
    return super.build();
  }

  @override
  void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
    //此處就直接刷新添加的監(jiān)聽子Element了,不各種super了
    dependent.markNeedsBuild();
    // super.notifyDependent(oldWidget, dependent);
  }

  @override
  void unmount() {
    _value.removeListener(_callBack);
    _value.dispose();
    super.unmount();
  }
}

Provider

class Provider { // 不包含核心邏輯,僅僅是封裝 
  /// 獲取EasyP實例
  /// 獲取實例的時候,listener參數(shù)老是寫錯,這邊直接用倆個方法區(qū)分了
  static T of<T extends ChangeNotifier>(BuildContext context) {
    return _getInheritedElement<T>(context).value;
  }

  /// 注冊監(jiān)聽控件
  static T register<T extends ChangeNotifier>(BuildContext context) {
    var element = _getInheritedElement<T>(context);
    context.dependOnInheritedElement(element);
    return element.value;
  }

  /// 獲取距離當前Element最近繼承InheritedElement<T>的組件
  //調(diào)用dependOnInheritedWidgetOfExactType() 和 getElementForInheritedWidgetOfExactType()的區(qū)別就是前者會注冊依賴關(guān)系初婆,而后者不會
  static EasyPInheritedElement<T>
      _getInheritedElement<T extends ChangeNotifier>(BuildContext context) {
    var inheritedElement = context
            .getElementForInheritedWidgetOfExactType<EasyPInherited<T>>()
        as EasyPInheritedElement<T>?;

    if (inheritedElement == null) {
      throw EasyPNotFoundException(T);
    }

    return inheritedElement;
  }
}

class EasyPNotFoundException implements Exception {
  EasyPNotFoundException(this.valueType);

  final Type valueType;

  @override
  String toString() => 'Error: Could not find the EasyP<$valueType>';
}

builder :

class EasyPBuilder<T extends ChangeNotifier> extends StatelessWidget {
  const EasyPBuilder(
    this.builder, {
    Key? key,
  }) : super(key: key);

  final Widget Function() builder;

  @override
  Widget build(BuildContext context) {
    EasyP.register<T>(context);
    return builder();
  }
}

案例:

自己寫一個封裝的controller()

3個核心類:

build:

Provider:

ChangeNotifierProvider:

點擊事件: 數(shù)據(jù)變化調(diào)用provide里面的變化方法,

ui更新, 也是拿provider的值

 Widget _buildPage(BuildContext context) {
    final easyP = EasyP.of<CounterEasyP>(context);

    return Scaffold(
      appBar: AppBar(title: Text('自定義狀態(tài)管理框架-EasyP范例')),
      body: Center(
        child: EasyPBuilder<CounterEasyP>(() {
          return Text(
            '點擊了 ${easyP.count} 次',
            style: TextStyle(fontSize: 30.0),
          );
        }),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => easyP.increment(),
        child: Icon(Icons.add),
      ),
    );
  }
}

創(chuàng)建了一個widget:

class CounterEasyPPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierEasyP(
      create: (BuildContext context) => CounterEasyP(),
      builder: (context) => _buildPage(context),
    );
  }

把需要更新的widget傳入到構(gòu)造方法中去了! ChangeNotifier傳入, buildcontext也在

5. 簡單的狀態(tài)管理:

對于數(shù)據(jù)的跨層傳遞弊琴,F(xiàn)lutter還提供了三種方案:InheritedWidget敲董、Notification和EventBus臣缀。接下來,我將依次為你講解這三種方案锣杂。


b2a78dbefdf30895504b2017355ae066.png

5.1 InheritedWidget 原理如上

5.2 Notification

數(shù)據(jù)流動方式是從子Widget向上傳遞至父Widget。這樣的數(shù)據(jù)傳遞機制適用于子Widget狀態(tài)變更踱蠢,發(fā)送通知上報的場景

Notification是一種用于在小部件樹中傳遞信息的機制棋电,它可以用于實現(xiàn)子樹中的特定部分之間的通信茎截。Notification并不像狀態(tài)管理或全局狀態(tài)傳遞那樣普遍,它主要用于特定場景下的通信赶盔,比如當某個事件發(fā)生時企锌,需要在小部件樹的各個部分之間傳遞消息。Notification的工作方式是通過Notification對象在小部件樹中傳遞于未,然后從父級小部件開始逐級向上冒泡撕攒,直到找到一個處理該通知的小部件為止陡鹃。每個處理通知的小部件可以根據(jù)需要執(zhí)行特定的操作。你可以把InheritedWidget 理解為從上到下傳遞抖坪、共享的方式脊阴,而Notification則是從下往上状知。Notification它提供了dispatch方法,沿著context對應(yīng)的Element節(jié)點向上逐層發(fā)送通知。

跨組件事件傳遞

5.3 EventBus

在組件之間如果有事件需要傳遞,一方面可以一層層來傳遞,另一方面我們也可以使用一個EventBus工具來完成雄人。

其實EventBus在Vue、React中都是一種非常常見的跨組件通信的方式:

EventBus相當于是一種訂閱者模式,通過一個全局的對象來管理翻具;

這個EventBus我們可以自己實現(xiàn)工禾,也可以使用第三方的EventBus槽畔;

    無需發(fā)布者與訂閱者之間存在父子關(guān)系的數(shù)據(jù)同步機制早直。

無論是InheritedWidget還是Notificaiton祥得,它們的使用場景都需要依靠Widget樹,也就意味著只能在有父子關(guān)系的Widget之間進行數(shù)據(jù)共享。但是,組件間數(shù)據(jù)傳遞還有一種常見場景:這些組件間不存在父子關(guān)系。這時,事件總線EventBus就登場了闪萄。

     事件總線是在Flutter中實現(xiàn)跨組件通信的機制三椿。它遵循發(fā)布/訂閱模式蛋叼,允許訂閱者訂閱事件,當發(fā)布者觸發(fā)事件時,訂閱者和發(fā)布者之間可以通過事件進行交互砚哆。發(fā)布者和訂閱者之間無需有父子關(guān)系关炼,甚至非Widget對象也可以發(fā)布/訂閱社痛。這些特點與其他平臺的事件總線機制是類似的。

     總結(jié):  

這里我準備了一張表格匈织,把屬性傳值掸哑、InheritedWidget择浊、Notification與EventBus這四種數(shù)據(jù)共享方式的特點和使用場景做了簡單總結(jié)糕篇,供你參考:

6. 下面是對Provider、BLoC余舶、Redux绊谭、GetX蒙保、Riverpod和MobX等Flutter狀態(tài)管理庫的一些對比:

狀態(tài)管理比較.jpg

`1). InheritedWidget:在 Flutter 中硅堆,所有 Widget 都是通過父 Widget 構(gòu)建出來的哭廉,父 Widget 可以通過 InheritedWidget 共享狀態(tài)給子 Widget,子 Widget 可以通過調(diào)用 InheritedWidget.of() 方法來獲取共享的狀態(tài)乌企。

2). Provider:

  • 適用場景: 適用于中小型應(yīng)用,需要在多個層級共享狀態(tài)的場景脖岛。
  • 特點: 輕量級,使用InheritedWidget來共享狀態(tài),支持各種類型的狀態(tài)质帅,易于上手。
  • 優(yōu)勢: 簡單易用登渣,不需要大量的額外代碼,具有高性能毡泻,適用于簡單的狀態(tài)共享胜茧。
  • 劣勢: 在大型應(yīng)用中可能難以管理復雜的狀態(tài)。

狀態(tài)管理混亂仇味,雖然用了 provider 來做狀態(tài)管理呻顽,但一些代碼如:異步請求、事件響應(yīng)等還是會摻雜在UI頁面的代碼里面丹墨,一旦頁面的各種 Widget 多了起來之后廊遍,顯得非常嚴重,而且對業(yè)務(wù)邏輯的測試也不方便贩挣,多個組件可能需要共享相同的數(shù)據(jù)或狀態(tài)喉前,需要在每個組件中分別創(chuàng)建 Provider 實例,容易導致代碼冗余揽惹,如果只需要更新頁面的部分 Widget 使用Provider 還會導致嵌套過深被饿,也可能導致性能問題、狀態(tài)不一致以及難以追蹤的錯誤

3). ScopedModel:ScopedModel 也可以實現(xiàn)狀態(tài)共享搪搏,但它的思想是將數(shù)據(jù)放在一個共享的 Model 中狭握,然后讓需要用到這些數(shù)據(jù)的 Widget 注冊監(jiān)聽該 Model,當 Model 的數(shù)據(jù)改變時疯溺,通知監(jiān)聽它的 Widget 更新论颅。
4). Stream

Stream是一種用于在應(yīng)用程序中管理狀態(tài)和數(shù)據(jù)流的重要工具。Stream是異步數(shù)據(jù)流的抽象表示囱嫩,它可以在應(yīng)用程序中傳遞和監(jiān)聽數(shù)據(jù)的變化恃疯。但是它和Flutter關(guān)系并不大,它是通過純dart去實現(xiàn)的墨闲。你可以理解為flutter只是通過StreamBuilder去構(gòu)建了一個Stream通道今妄。它的使用其實也并沒有復雜太多,通常只需要創(chuàng)建StreamController鸳碧,然后去監(jiān)聽控制器(可以直接去監(jiān)聽StreamController盾鳞,然后通過setState更新UI,也可以通過StreamBuilder)瞻离,最后將更新后的數(shù)據(jù)通過Stream的sink屬性添加到Stream中即可腾仅。知名的狀態(tài)管理庫Bloc,就是基于Stream的封裝套利。

5). BLoC:

BLoC 算是 Flutter 早期比較知名的狀態(tài)管理框架, 它是基于事件驅(qū)動來實現(xiàn)的狀態(tài)管理

BLoC 是業(yè)務(wù)邏輯組件的縮寫推励,它使用 Streams 和Provider 庫將業(yè)務(wù)邏輯和 UI 分離開來鹤耍,可以用來管理狀態(tài)和處理用戶輸入。作者在這里使用了Bloc用于狀態(tài)管理

基于 Stream 的封裝可以更方便做一些事件狀態(tài)的監(jiān)聽和轉(zhuǎn)換

  • 適用場景: 適用于復雜的應(yīng)用验辞,需要分離業(yè)務(wù)邏輯和UI的場景稿黄。
  • 特點: 通過Streams管理狀態(tài)和業(yè)務(wù)邏輯,將界面層與業(yè)務(wù)邏輯層分開跌造,適合中大型應(yīng)用抛猖。
  • 優(yōu)勢: 適合處理復雜的狀態(tài)變化和異步操作,便于測試和維護鼻听。
  • 劣勢: 在簡單應(yīng)用中可能顯得過于復雜。需要寫更多的代碼联四,開發(fā)節(jié)奏會有點影響

6). Redux:

  • Redux 是一種狀態(tài)管理模式撑碴,它將狀態(tài)和狀態(tài)更新封裝在一個可預測的單向數(shù)據(jù)流中,可以用于處理應(yīng)用程序的復雜狀態(tài)朝墩。

前端開始者對 redux 可能會更熟悉一些

在 flutter_redux 中醉拓,開發(fā)者的每個操作都只是一個 Action ,而這個行為所觸發(fā)的邏輯完全由 middlewarereducer 決定

  • 適用場景: 適用于需要管理大量復雜狀態(tài)的應(yīng)用收苏。
  • 特點: 基于單一狀態(tài)源和不可變狀態(tài)亿卤,通過Actions和Reducers來管理狀態(tài)變化。
  • 優(yōu)勢: 嚴格的狀態(tài)管理鹿霸,適用于大型應(yīng)用排吴,具有強大的開發(fā)工具和中間件。
  • 劣勢: 在小型應(yīng)用中可能過于繁瑣懦鼠,學習曲線較陡`
    7). ;不在維護了2012年后沒有維護了! 第三方框架, 咸魚Fish Redux的框架

8). Riverpod:

適用場景: 適用于需要更強大钻哩、更簡單的狀態(tài)管理和依賴注入的場景。
特點: 基于Provider的升級版本肛冶,提供更簡單街氢、更強大的API,支持多種狀態(tài)管理模式睦袖。
優(yōu)勢: 代碼清晰珊肃,性能高效,支持多種狀態(tài)管理模式馅笙,適用于各種規(guī)模的項目伦乔。
劣勢: 相對較新的庫,社區(qū)可能還在成長延蟹。
隨著 Flutter 的發(fā)展评矩,這些年 Flutter 上的狀態(tài)管理框架如“雨后春筍”般層出不窮,而近一年以來最受官方推薦的狀態(tài)管理框架無疑就是 Riverpod
直觀的就是 Riverpod 中的 Provider 可以隨意寫成全局, 如果說 Riverpod 最明顯的特點是什么阱飘,那就是外部不依賴 BuildContext
它的作者也是 Provider 的作者斥杜,同時也是 Flutter 官方推薦的狀態(tài)管理庫
如何實現(xiàn)不依賴 BuildContext?
在 Riverpod 里基本是每一個 “Provider” 都會有一個自己的 “Element” 虱颗,然后通過 WidgetRef 去 Hook 后成為 BuildContext 的替代,所以這就是 Riverpod 不依賴 Context 的 “魔法” 之一
9). GetX:

適用場景: 適用于快速開發(fā)和中小型應(yīng)用蔗喂,需要輕量級狀態(tài)管理和依賴注入的場景忘渔。
特點: 簡單易用,提供狀態(tài)管理缰儿、依賴注入和路由導航的綜合解決方案畦粮。
優(yōu)勢: 低學習曲線,高性能乖阵,適用于快速迭代的小型項目宣赔。
劣勢: 對于大型復雜應(yīng)用,可能需要更復雜的狀態(tài)管理方案瞪浸。

10). MobX:
適用場景: 適用于需要響應(yīng)式編程和可觀察對象的場景儒将。
特點: 通過可觀察對象和反應(yīng)式編程來管理狀態(tài),支持多種數(shù)據(jù)變化方式对蒲。提高開發(fā)效率
優(yōu)勢: 簡化了狀態(tài)管理钩蚊,具有響應(yīng)式編程的特點,易于學習和使用蹈矮。
劣勢: 相對較新的庫砰逻,可能在一些大型項目中缺乏一些高級功能。全家桶泛鸟,做的太多對于一些使用者來說是致命缺點蝠咆,需要解決的 Bug 也多
11). rxdart: 太老了

狀態(tài)管理.jpg
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市谈况,隨后出現(xiàn)的幾起案子勺美,更是在濱河造成了極大的恐慌,老刑警劉巖碑韵,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件赡茸,死亡現(xiàn)場離奇詭異,居然都是意外死亡祝闻,警方通過查閱死者的電腦和手機占卧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來联喘,“玉大人华蜒,你說我怎么就攤上這事』碓猓” “怎么了叭喜?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長蓖谢。 經(jīng)常有香客問我捂蕴,道長譬涡,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任啥辨,我火速辦了婚禮涡匀,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘溉知。我一直安慰自己陨瘩,他們只是感情好,可當我...
    茶點故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布级乍。 她就那樣靜靜地躺著舌劳,像睡著了一般。 火紅的嫁衣襯著肌膚如雪玫荣。 梳的紋絲不亂的頭發(fā)上蒿囤,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天,我揣著相機與錄音崇决,去河邊找鬼。 笑死底挫,一個胖子當著我的面吹牛恒傻,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播建邓,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼盈厘,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了官边?” 一聲冷哼從身側(cè)響起沸手,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎注簿,沒想到半個月后契吉,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡诡渴,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年捐晶,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片妄辩。...
    茶點故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡惑灵,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出眼耀,到底是詐尸還是另有隱情英支,我是刑警寧澤,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布哮伟,位于F島的核電站干花,受9級特大地震影響妄帘,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜把敢,卻給世界環(huán)境...
    茶點故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一寄摆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧修赞,春花似錦婶恼、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至割择,卻和暖如春眷篇,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背荔泳。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工蕉饼, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人玛歌。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓昧港,卻偏偏與公主長得像,于是被迫代替她去往敵國和親支子。 傳聞我的和親對象是個殘疾皇子创肥,可洞房花燭夜當晚...
    茶點故事閱讀 45,435評論 2 359

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