天天看點

Flutter-狀态管理

1.狀态管理原則

​StatefulWidget​

​的狀态應該被誰管理?Widget本身?父 Widget ?都會?還是另一個對象?答案是取決于實際情況!以下是管理狀态的最常見的方法:

  • Widget 管理自己的狀态。setState
  • Widget 管理子 Widget 狀态。
  • 混合管理(父 Widget 和子 Widget 都管理狀态)。
  • 全局狀态管理 (三方)

如何決定使用哪種管理方法?下面是官方給出的一些原則可以幫助你做決定:

  • 如果狀态是使用者資料,如複選框的選中狀态、滑塊的位置,則該狀态最好由父 Widget 管理。
  • 如果狀态是有關界面外觀效果的,例如顔色、動畫,那麼狀态最好由 Widget 本身來管理。
  • 如果某一個狀态是不同 Widget 共享的則最好由它們共同的父 Widget 管理。

一般的原則是:如果狀态是元件私有的,則應該由元件自己管理;如果狀态要跨元件共享,則該狀态應該由各個元件共同的父元素來管理。對于元件私有的狀态管理很好了解,但對于跨元件共享的狀态,管理的方式就比較多了。

2.官方狀态管理方式

2.1全局事件總線EventBus

它是一個觀察者模式的實作,通過它就可以實作跨元件狀态同步:狀态持有方(釋出者)負責更新、釋出狀态,狀态使用方(觀察者)監聽狀态改變事件來執行一些操作。

Flutter-狀态管理
Flutter-狀态管理
Flutter-狀态管理

我們可以發現,通過觀察者模式來實作跨元件狀态共享有一些明顯的缺點:

  1. 必須顯式定義各種事件,不好管理。
  2. 訂閱者必須需顯式注冊狀态改變回調,也必須在元件銷毀時手動去解綁回調以避免記憶體洩露。

2.2InheritedWidget

比如我們在應用的根 widget 中通過​

​InheritedWidget​

​共享了一個資料,那麼我們便可以在任意子widget 中來擷取該共享的資料!

當​

​InheritedWidget​

​​資料發生變化時,可以自動更新依賴的子孫元件!利用這個特性,我們可以将需要跨元件共享的狀态儲存在​

​InheritedWidget​

​​中,然後在子元件中引用​

​InheritedWidget​

用法: •自定義Widget繼承自lnheritedWidget, 并且自定義static of方法,用于child擷取目前執行個體 •實作 Inheritedwidtet 類中的 updateShoulaNotify 方法,用于傳回update條件; 當資料變化時,調用自定義widget的State類中的setState(方法,觸發整棵Inheritedwldget tree的更新

定義一個共享資料的InheritedWidget,需要繼承自InheritedWidget

class HYCounterWidget extends InheritedWidget {
  // 1.要共享的資料
  final int counter;

  // 2.定義構造方法
  HYCounterWidget({this.counter, Widget child}): super(child: child);

  // 3.擷取元件最近的目前InheritedWidget
  static HYCounterWidget of(BuildContext context) {
    // 沿着Element樹, 去找到最近的HYCounterElement, 從Element中取出Widget對象
    return context.dependOnInheritedWidgetOfExactType();
  }

  // 4.絕對要不要回調State中的didChangeDependencies
  // 如果傳回true: 執行依賴當期的InheritedWidget的State中的didChangeDependencies
  @override
  bool updateShouldNotify(HYCounterWidget oldWidget) {
    return oldWidget.counter != counter;
  }
}
      
Flutter-狀态管理
Flutter-狀态管理

優點:

  1. 自動訂閱

InheritedWidget内部會維護一個Widget的Map,當子Widget調用Context#inheritFromWidgetOfExactType時就會自動将子Widget存入Map中,并且将InheritedWidget傳回給子Widget。

  1. 自動通知

InheritedWidget重建後悔自動觸發InheritElement的Update方法。

缺點:

  1. 無法分離視圖邏輯和業務邏輯。
  2. 無法定向通知/指向性通知。

InheritedWidget不會區分Widget是否需要更新的問題,每次更新都會通知所有的子Widget。是以需要配合StreamBuilder來解決問題。

StreamBuilder是Flutter封裝好的監聽Stream資料變化的Widget,本質上是一個​

​StatefulWidget​

​​,内部通過​

​Stream.listen()​

​​來監聽傳入的​

​stream​

​​的變化,當監聽到有變化時就調用​

​setState()​

​方法來更新Widget。

關于​

​stream​

​的介紹的文章到處都有,别人寫的也很詳細,這裡就不再贅述了。

有了​

​StreamBuilder​

​​,我們可以在子Widget上通過​

​StreamBuilder​

​​來監聽InheritedWidget中的​

​Stream​

​的資料變化,然後判斷是否需要更新目前的子Widget,這樣就完成了資料的定向通知。

2.3Provider

Provider是目前官方推薦的全局狀态管理工具,由社群作者Remi Rousselet 和 Flutter Team共同編寫。

它對InheritedWidget進行了上層封裝,緻力解決原生setState方案的props臃腫、展示與邏輯耦合問題。

它的原理就是綁定 Inheritedwidget 與依 賴它的子孫元件的依賴關系,并且當 Inheritedwidget 資料發生變化時,可以自動更新依賴的子孫 元件。

Provider将頁面分為業務和視圖兩層,并定義Notifier、Consumer兩個核心概念:

  • Notifier負責實作業務邏輯,且在資料更新時發出通知。
  • Consumer負責實作界面邏輯,并在資料更新時更新自身,以及使用者互動時調用Notifier方法。
Flutter-狀态管理

元件間通信依賴公共Notifier父節點完成,其流程為:

  1. 元件A調用Notifier方法更新資料。
  2. Notifier節點通知元件B資料更新。

優勢

  1. 方案涉及概念少,上手成本低。

劣勢

  1. 資料流分為業務、視圖兩層。項目規模變大時,業務層複雜度容易指數級增長。
  2. 元件通信方式依賴公共Notifier父節點,資料同步與元件樹結構強耦合,項目維護成本高。
  3. 元件不可插拔,元件擷取資料依賴Notifier父節點,與元件樹結構強耦合,項目維護成本高。

使用之前,我們需要先引入對它的依賴,截止這篇文章,Provider的最新版本為4.0.4:

dependencies:
  provider: ^4.0.4      

2.3.1 Provider基本使用

在使用Provider的時候,我們主要關心三個概念:

  • ChangeNotifier:真正資料(狀态)存放的地方
  • ChangeNotifierProvider:Widget樹中提供資料(狀态)的地方,會在其中建立對應的ChangeNotifier
  • Consumer:Widget樹中需要使用資料(狀态)的地方
Flutter-狀态管理

我們先來完成一個簡單的案例,将官方計數器案例使用Provider來實作:

第一步:建立自己的ChangeNotifier

我們需要一個ChangeNotifier來儲存我們的狀态,是以建立它

  • 這裡我們可以使用繼承自ChangeNotifier,也可以使用混入,這取決于機率是否需要繼承自其它的類
  • 我們使用一個私有的_counter,并且提供了getter和setter
  • 在setter中我們監聽到_counter的改變,就調用notifyListeners方法,通知所有的Consumer進行更新
class CounterProvider extends ChangeNotifier {
  int _counter = 100;
  int get counter {
    return _counter;
  }
  set counter(int value) {
    _counter = value;
    notifyListeners();
  }
}
      

第二步:在Widget Tree中插入ChangeNotifierProvider

我們需要在Widget Tree中插入ChangeNotifierProvider,以便Consumer可以擷取到資料:

  • 将ChangeNotifierProvider放到了頂層,這樣友善在整個應用的任何地方可以使用CounterProvider
void main() {
  runApp(ChangeNotifierProvider(
    create: (context) => CounterProvider(),
    child: MyApp(),
  ));
}
      

第三步:在首頁中使用Consumer引入和修改狀态

  • 引入位置一:在body中使用Consumer,Consumer需要傳入一個builder回調函數,當資料發生變化時,就會通知依賴資料的Consumer重新調用builder方法來建構;
  • 引入位置二:在floatingActionButton中使用Consumer,當點選按鈕時,修改CounterNotifier中的counter資料;
class HYHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("清單測試"),
      ),
      body: Center(
        child: Consumer<CounterProvider>(
          builder: (ctx, counterPro, child) {
            return Text("目前計數:${counterPro.counter}", style: TextStyle(fontSize: 20, color: Colors.red),);
          }
        ),
      ),
      floatingActionButton: Consumer<CounterProvider>(
        builder: (ctx, counterPro, child) {
          return FloatingActionButton(
            child: child,
            onPressed: () {
              counterPro.counter += 1;
            },
          );
        },
        child: Icon(Icons.add),
      ),
    );
  }
}
      

Consumer的builder方法解析:

  • 參數一:context,每個build方法都會有上下文,目的是知道目前樹的位置
  • 參數二:ChangeNotifier對應的執行個體,也是我們在builder函數中主要使用的對象
  • 參數三:child,目的是進行優化,如果builder下面有一顆龐大的子樹,當模型發生改變的時候,我們并不希望重新build這顆子樹,那麼就可以将這顆子樹放到Consumer的child中,在這裡直接引入即可(注意我案例中的Icon所放的位置)
Flutter-狀态管理

案例效果

步驟四:建立一個新的頁面,在新的頁面中修改資料

class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("第二個頁面"),
      ),
      floatingActionButton: Consumer<CounterProvider>(
        builder: (ctx, counterPro, child) {
          return FloatingActionButton(
            child: child,
            onPressed: () {
              counterPro.counter += 1;
            },
          );
        },
        child: Icon(Icons.add),
      ),
    );
  }
}
      
Flutter-狀态管理

第二個頁面修改資料

2.3.2. Provider.of的弊端

事實上,因為Provider是基于InheritedWidget,是以我們在使用ChangeNotifier中的資料時,我們可以通過Provider.of的方式來使用,比如下面的代碼:

Text("目前計數:${Provider.of<CounterProvider>(context).counter}",
  style: TextStyle(fontSize: 30, color: Colors.purple),
),
      

我們會發現很明顯上面的代碼會更加簡潔,那麼開發中是否要選擇上面這種方式了?

  • 答案是否定的,更多時候我們還是要選擇Consumer的方式。

為什麼呢?因為Consumer在重新整理整個Widget樹時,會盡可能少的rebuild Widget。

方式一:Provider.of的方式完整的代碼:

  • 當我們點選了floatingActionButton時,HYHomePage的build方法會被重新調用。
  • 這意味着整個HYHomePage的Widget都需要重新build
class HYHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print("調用了HYHomePage的build方法");
    return Scaffold(
      appBar: AppBar(
        title: Text("Provider"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text("目前計數:${Provider.of<CounterProvider>(context).counter}",
              style: TextStyle(fontSize: 30, color: Colors.purple),
            )
          ],
        ),
      ),
      floatingActionButton: Consumer<CounterProvider>(
        builder: (ctx, counterPro, child) {
          return FloatingActionButton(
            child: child,
            onPressed: () {
              counterPro.counter += 1;
            },
          );
        },
        child: Icon(Icons.add),
      ),
    );
  }
}
      

方式二:将Text中的内容采用Consumer的方式修改如下:

  • 你會發現HYHomePage的build方法不會被重新調用;
  • 設定如果我們有對應的child widget,可以采用上面案例中的方式來組織,性能更高;
Consumer<CounterProvider>(builder: (ctx, counterPro, child) {
  print("調用Consumer的builder");
  return Text(
    "目前計數:${counterPro.counter}",
    style: TextStyle(fontSize: 30, color: Colors.red),
  );
}),
      

2.3.3. Selector的選擇

Consumer是否是最好的選擇呢?并不是,它也會存在弊端

  • 比如當點選了floatingActionButton時,我們在代碼的兩處分别列印它們的builder是否會重新調用;
  • 我們會發現隻要點選了floatingActionButton,兩個位置都會被重新builder;
  • 但是floatingActionButton的位置有重新build的必要嗎?沒有,因為它是否在操作資料,并沒有展示;
  • 如何可以做到讓它不要重新build了?使用Selector來代替Consumer
Flutter-狀态管理

Select的弊端

我們先直接實作代碼,在解釋其中的含義:

floatingActionButton: Selector<CounterProvider, CounterProvider>(
  selector: (ctx, provider) => provider,
  shouldRebuild: (pre, next) => false,
  builder: (ctx, counterPro, child) {
    print("floatingActionButton展示的位置builder被調用");
    return FloatingActionButton(
      child: child,
      onPressed: () {
        counterPro.counter += 1;
      },
    );
  },
  child: Icon(Icons.add),
),
      

Selector和Consumer對比,不同之處主要是三個關鍵點:

  • 關鍵點1:泛型參數是兩個
  • 泛型參數一:我們這次要使用的Provider
  • 泛型參數二:轉換之後的資料類型,比如我這裡轉換之後依然是使用CounterProvider,那麼他們兩個就是一樣的類型
  • 關鍵點2:selector回調函數
  • 轉換的回調函數,你希望如何進行轉換
  • S Function(BuildContext, A) selector
  • 我這裡沒有進行轉換,是以直接将A執行個體傳回即可
  • 關鍵點3:是否希望重新rebuild
  • 這裡也是一個回調函數,我們可以拿到轉換前後的兩個執行個體;
  • bool Function(T previous, T next);
  • 因為這裡我不希望它重新rebuild,無論資料如何變化,是以這裡我直接return false;
Flutter-狀态管理

Selector的使用

這個時候,我們重新測試點選floatingActionButton,floatingActionButton中的代碼并不會進行rebuild操作。

是以在某些情況下,我們可以使用Selector來代替Consumer,性能會更高。

2.3.4. MultiProvider

在開發中,我們需要共享的資料肯定不止一個,并且資料之間我們需要組織到一起,是以一個Provider必然是不夠的。

我們在增加一個新的ChangeNotifier

import 'package:flutter/material.dart';

class UserInfo {
  String nickname;
  int level;

  UserInfo(this.nickname, this.level);
}

class UserProvider extends ChangeNotifier {
  UserInfo _userInfo = UserInfo("why", 18);

  set userInfo(UserInfo info) {
    _userInfo = info;
    notifyListeners();
  }

  get userInfo {
    return _userInfo;
  }
}
      

如果在開發中我們有多個Provider需要提供應該怎麼做呢?

方式一:多個Provider之間嵌套

  • 這樣做有很大的弊端,如果嵌套層級過多不友善維護,擴充性也比較差
runApp(ChangeNotifierProvider(
    create: (context) => CounterProvider(),
    child: ChangeNotifierProvider(
      create: (context) => UserProvider(),
      child: MyApp()
    ),
  ));
      

方式二:使用MultiProvider

runApp(MultiProvider(
  providers: [
    ChangeNotifierProvider(create: (ctx) => CounterProvider()),
    ChangeNotifierProvider(create: (ctx) => UserProvider()),
  ],
  child: MyApp(),
));      

provider自己的想法總結 重要!:

1. 建立自己需要共享的資料
2. 在應用程式的頂層ChangeNotifierProvider
3. 在其它位置使用共享的資料
-  Provider.of: 當Provider中的資料發生改變時, Provider.of所在的Widget整個build方法都會重新建構
-  Consumer(相對推薦): 當Provider中的資料發生改變時, 執行重新執行Consumer的builder
-  Selector: 
   1.selector方法(作用,對原有的資料進行轉換)     
   2.shouldRebuild(作用,要不要重新建構)      
Flutter-狀态管理
Flutter-狀态管理
Flutter-狀态管理
Flutter-狀态管理
Flutter-狀态管理

3.三方狀态管理包

3.1 BLoC

Flutter-狀态管理

BLoC是Business Logic Component設計模式在Flutter上的實作。它緻力于解耦展示與邏輯分離,提升代碼的可測試性。

整體結構上,BLoC類似多Store的redux并增加了大量響應式程式設計特性,相較于redux而言:

  • 多Store解決了業務邏輯集中,複雜度容易指數級增長的問題,加強了不同業務邏輯間的作用域隔離。
  • 響應式程式設計特性減少了redux中的模闆代碼。

整體資料流如下圖:

Flutter-狀态管理

優勢

  1. 資料流分為全局多Store和視圖兩層。項目規模變大時,合理配置設定多個Store承載的業務邏輯可避免業務層複雜度過高。
  2. 元件通信方式依賴全局Store,資料同步與元件樹結構解耦,便于項目維護。
  3. 元件可插拔,元件擷取資料依賴全局Store,與元件樹解耦,便于項目維護。

劣勢

  1. 缺少局部Store無法實作業務邏輯在元件範圍的自治。

3.2 fish-redux

  • page:總頁面,注冊effect,reducer,component,adapter的功能,相關的配置都在此頁面操作
  • state:這地方就是我們存放子子產品變量的地方;初始化變量和接受上個頁面參數,也在此處,是個很重要的子產品
  • view:主要是我們寫頁面的子產品
  • action:這是一個非常重要的子產品,所有的事件都在此處定義和中轉
  • effect:相關的業務邏輯,網絡請求等等的“副作用”操作,都可以寫在該子產品
  • reducer:該子產品主要是用來更新資料的,也可以寫一些簡單的邏輯或者和資料有關的邏輯操作
  • view ---> action(事件定義和中轉) ---> effect (業務邏輯  網絡)---> reducer(更新資料)
Flutter-狀态管理

優點:

  1. 資料集中管理,架構自動完成reducer合并。
  2. 元件分治管理,元件之間以及和容器之間互相隔離。
  3. View、Reducer、Effect隔離。易于編寫複用。
  4. 聲明式配置組裝。
  5. 良好的擴充性。

個人感覺fish_redux的設計适用于複雜的業務場景,加上複雜的目錄結構以及相關概念,不太适合普通的資料不太複雜的業務。

fish_redux使用詳解---看完就會用!

3.3 redux

fish-redux将頁面分為類redux的Store(業務)和Component(視圖)兩層,每個視圖元件同樣是redux标準結構。

資料更新時,視圖元件首先會嘗試在自身Store中進行計算并更新自身,當發現缺少資料或更新将影響其他視圖元件時,則将資料的改動廣播至全局Store處理。

單個視圖元件的資料流如下圖:

優勢

  1. 資料流分為全局Store、元件Store群組件視圖三層。項目規模變大時,合理配置設定全局Store、元件Store承載的業務邏輯可避免業務層複雜度過高。同時元件Store的設計幫助實作業務邏輯在元件範圍的自治。
  2. 元件通信方式依賴全局Store,資料同步與元件樹結構解耦,便于項目維護。
  3. 元件可插拔,元件擷取資料依賴全局Store,與元件樹結構解耦,便于項目維護。

劣勢

  1. redux模式下模闆代碼多,合理使用的門檻較高。
  2. 項目規模變大時,單一的全局Store仍有複雜度過高的可能。

繼續閱讀