扣丁書屋

Flutter 狀態管理框架 Provider 和 Get 分析

狀態管理一直是 Flutter 開發中一個火熱的話題。談到狀態管理框架,社區也有諸如有以 Provider 和 Get 為代表的多種方案,它們有各自的優缺點。

面對這么多的選擇,你可能會想:「我需要使用狀態管理么?哪種框架更適合我?」本文將從作者的實際開發經驗出發,分析狀態管理解決的問題以及思路,希望能幫助你做出選擇。

為什么需要狀態管理?

首先,為什么需要狀態管理?根據筆者的經驗,這是因為 Flutter 基于 聲明式 構建 UI,使用狀態管理的目的之一就是解決「聲明式」開發帶來的問題。

「聲明式」開發是一種區別于傳原生的方式,所以我們沒有在原生開發中聽到過狀態管理,那如何理解「聲明式」開發呢?

「聲明式」VS「命令式」分析

以最經典的的計數器例子分析:

通過計數器 app 理解 Flutter 的「聲明式」和「命令式」如上圖所示:點擊右下角按鈕,顯示的文本數字加一。Android 中可以這么實現:當右下角按鈕點中時,拿到 TextView 的對象,手動設置其展示的文本。

實現代碼如下:

// 一、定義展示的內容
private int mCount =0;

// 二、中間展示數字的控件 TextView
private TextView mTvCount;

// 三、關聯 TextView 與 xml 中的組件
mTvCount = findViewById(R.id.tv_count)

// 四、點擊按鈕控制組件更新
private void increase( ){ 
 mCount++;
 mTvCounter.setText(mCount.toString()); 
}

而在 Flutter 中,我們只需要使變量增加之后調用 setState((){}) 即可,setState 會刷新整個頁面,使得中間展示的值進行變更。

// 一、聲明變量
int _counter =0; 

// 二、展示變量 
Text('$_counter')

//  三、變量增加,更新界面
setState(() {
   _counter++; 
});

可以發現,Flutter 中只對 _counter 屬性進行了修改,并沒有對 Text 組件進行任何的操作,整個界面隨著狀態的改變而改變。

所以在 Flutter 中有這么一種說法: UI = f(state):

上面的例子中,狀態 (state) 就是 _counter 的值,調用 setState 驅動 f build 方法生成新的 UI。

那么,聲明式有哪些優勢,并帶來了哪些問題呢?

優勢: 讓開發者擺脫組件的繁瑣控制,聚焦于狀態處理

習慣 Flutter 開發之后,回到原生平臺開發,你會發現當多個組件之間相互關聯時,對于 View 的控制非常麻煩。

而在 Flutter 中我們只需要處理好狀態即可 (復雜度轉移到了狀態 -> UI 的映射,也就是 Widget 的構建)。包括 Jetpack Compose、Swift 等技術的最新發展,也是在朝著「聲明式」的方向演進。

聲明式開發帶來的問題

沒有使用狀態管理,直接「聲明式」開發的時候,遇到的問題總結有三個:

  1. 邏輯和頁面 UI 耦合,導致無法復用/單元測試、修改混亂等
  2. 難以跨組件 (跨頁面) 訪問數據
  3. 無法輕松的控制刷新范圍 (頁面 setState 的變化會導致全局頁面的變化)

接下來,我先帶領大家逐個了解這些問題,下一章向大家詳細描述狀態管理框架如何解決這些問題。

1) 邏輯和頁面 UI 耦合,導致無法復用/單元測試、修改混亂等

一開始業務不復雜的時候,所有的代碼都直接寫到 widget 中,隨著業務迭代,文件越來越大,其他開發者很難直觀地明白里面的業務邏輯。并且一些通用邏輯,例如網絡請求狀態的處理、分頁等,在不同的頁面來回粘貼。

這個問題在原生上同樣存在,后面也衍生了諸如 MVP 設計模式的思路去解決。

2) 難以跨組件 (跨頁面) 訪問數據

第二點在于跨組件交互,比如在 Widget 結構中,一個子組件想要展示父組件中的 name 字段,可能需要層層進行傳遞。

又或者是要在兩個頁面之間共享篩選數據,并沒有一個很優雅的機制去解決這種跨頁面的數據訪問。

3) 無法輕松的控制刷新范圍 (頁面 setState 的變化會導致全局頁面的變化)

最后一個問題也是上面提到的優點,很多場景我們只是部分狀態的修改,例如按鈕的顏色。但是整個頁面的 setState 會使得其他不需要變化的地方也進行重建,帶來不必要的開銷。

Provider、Get 狀態管理框架設計分析

Flutter 中狀態管理框架的核心在于這三個問題的解決思路,下面一起看看 Provider、Get 是如何解決的:

解決邏輯和頁面 UI 耦合問題

傳統的原生開發同樣存在這個問題,Activity 文件也可能隨著迭代變得難以維護,這個問題可以通過 MVP 模式進行解耦。

簡單來說就是將 View 中的邏輯代碼抽離到 Presenter 層,View 只負責視圖的構建。

這也是 Flutter 中幾乎所有狀態管理框架的解決思路,上圖的 Presenter 你可以認為是 Get 中的 GetController、Provider 中的 ChangeNotifier 或者 Bloc 中的 Bloc。值得一提的是,具體做法上 Flutter 和原生 MVP 框架有所不同。

我們知道在經典 MVP 模式中,一般 View 和 Presenter 以接口定義自身行為 (action),相互持有接口進行調用 。

但 Flutter 中不太適合這么做,從 Presenter → View 關系上 View 在 Flutter 中對應 Widget,但在 Flutter 中 Widget 只是用戶聲明 UI 的配置,直接控制 Widget 實例并不是好的做法。

而在從 View → Presenter 的關系上,Widget 可以確實可以直接持有 Presenter,但是這樣又會帶來難以數據通信的問題。

這一點不同狀態管理框架的解決思路不一樣,從實現上他們可以分為兩大類:

  • 通過 Flutter 樹機制 解決,例如 Provider;
  • 通過 依賴注入,例如 Get。

1) 通過 Flutter 樹機制處理 V → P 的獲取

abstract class Element implements BuildContext { 
 /// 當前 Element 的父節點
 Element? _parent; 
}

abstract class BuildContext {
 /// 查找父節點中的T類型的State
 T findAncestorState0fType<T extends State>( );

 /// 遍歷子元素的element對象
 void visitChildElements(ElementVisitor visitor);

 /// 查找父節點中的T類型的 InheritedWidget 例如 MediaQuery 等
 T dependOnInheritedWidget0fExactType<T extends InheritedWidget>({ 
  Object aspect });
 ……
} 

Element 實現了父類 BuildContext 中操作樹結構的方法我們知道 Flutter 中存在三棵樹,Widget、Element 和 RenderObject。所謂的 Widget 樹其實只是我們描述組件嵌套關系的一種說法,是一種虛擬的結構。但 Element 和 RenderObject 在運行時實際存在,可以看到 Element 組件中包含了 _parent 屬性,存放其父節點。而它實現了 BuildContext 接口,包含了諸多對于樹結構操作的方法,例如 findAncestorStateOfType,向上查找父節點;visitChildElements 遍歷子節點。

在一開始的例子中,我們可以通過 context.findAncestorStateOfType 一層一層地向上查找到需要的 Element 對象,獲取 Widget 或者 State 后即可取出需要的變量。

provider 也是借助了這樣的機制,完成了 View -> Presenter 的獲取。通過 Provider.of 獲取頂層 Provider 組件中的 Present 對象。顯然,所有 Provider 以下的 Widget 節點,都可以通過自身的 context 訪問到 Provider 中的 Presenter,很好地解決了跨組件的通信問題。

2) 通過依賴注入的方式解決 V → P

樹機制很不錯,但依賴于 context,這一點有時很讓人抓狂。我們知道 Dart 是一種單線程的模型,所以不存在多線程下對于對象訪問的競態問題?;诖?Get 借助一個全局單例的 Map 存儲對象。通過依賴注入的方式,實現了對 Presenter 層的獲取,這樣在任意的類中都可以獲取到 Presenter。

這個 Map 對應的 key 是 runtimeType + tag,其中 tag 是可選參數,而 value 對應 Object,也就是說我們可以存入任何類型的對象,并且在任意位置獲取。

解決難以跨組件 (跨頁面) 訪問數據的問題

這個問題其實和上一部分的思考基本類似,所以我們可以總結一下兩種方案特點:

Provider

  • 依賴樹機制,必須基于 context
  • 提供了子組件訪問上層的能力

Get

  • 全局單例,任意位置可以存取
  • 存在類型重復,內存回收問題

解決高層級 setState 引起不必要刷新的問題

最后就是我們提到的高層級 setState 引起不必要刷新的問題,Flutter 通過采用觀察者模式解決,其關鍵在于兩步:

  1. 觀察者去訂閱被觀察的對象;
  2. 被觀察的對象通知觀察者。

系統也提供了 ValueNotifier 等組件的實現:

/// 聲明可能變化的數據
ValueNotifier<int> _statusNotifier = ValueNotifier(0); 

ValueListenableBuilder<int>(
 // 建立與 _statusNotifier 的綁定關系 
 valueListenable: _statusNotifier, 
 builder: (c, data, _) {
  return Text('$data'); 
})

///數據變化驅動 ValueListenableBuilder 局部刷新 
_statusNotifier.value += 1;

了解到最基礎的 [觀察者模式] 后,看看不同框架中提供的組件:

比如 Provider 中提供了 ChangeNotifierProvider:

class Counter extend ChangeNotifier { 
 int count = 0;

 /// 調用此方法更新所有觀察節點
 void increment() {
  count++;
  notifyListeners(); 
 }
}

void main() { 
 runApp(
  ChangeNotifierProvider(
   ///  返回一個實現 ChangeNotifier 接口的對象 
   create: (_) => Counter(),
   child: const MyApp( ), 
  ),
 );
 }

///  子節點通過 Consumer 獲取 Counter 對象 
Consumer<Counter>(
 builder:(_, counter, _) => Text(counter.count.toString()) 

還是之前計數器的例子,這里 Counter 繼承了 ChangeNotifier 通過頂層的 Provider 進行存儲。子節點通過 Consumer 即可獲取實例,調用了 increment 方法之后,只有對應的 Text 組件進行變化。

同樣的功能,在 Get 中,只需要提前調用 Get.put 方法存儲 Counter 對象,為 GetBuilder 組件指定 Counter 作為泛型。因為 Get 基于單例,所以 GetBuilder 可以直接通過泛型獲取到存入的對象,并在 builder 方法中暴露。這樣 Counter 便與組件建立了監聽關系,之后 Counter 的變動,只會驅動以它作為泛型的 GetBuilder 組件更新。

class Counter extends GetxController { 
 int count = 0;

 void increase() { 
  count++;
  update(); 
 }
}

/// 提前進行存儲
final counter = Get.put(Counter( )); 

/// 直接通過泛型獲取存儲好的實例
GetBuilder<Counter>(
 builder: (Counter counter) => Text('${counter.count}') ); 

實踐中的常見問題

在使用這些框架過程中,可能會遇到以下的問題:

Provider 中 context 層級過高

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Provider(
      create: (_) => const Count(),
      child: MaterialApp(
        home: Scaffold(
          body: Center(child: Text('${Provider.of<Counter>(context).count}')),
        ),
      ),
    );
  }
}

如代碼所示,當我們直接將 Provider 與組件嵌套于同一層級時,這時代碼中的 Provider.of(context) 運行時拋出 ProviderNotFoundException。因為此處我們使用的 context 來自于 MyApp,但 Provider 的 element 節點位于 MyApp 的下方,所以 Provider.of(context) 無法獲取到 Provider 節點。這個問題可以有兩種改法,如下方代碼所示:

改法 1: 通過嵌套 Builder 組件,使用子節點的 context 訪問:

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Provider(
      create: (_) => const Count(),
      child: MaterialApp(
        home: Scaffold(
          body: Center(
            child: Builder(builder: (builderContext) {
              return Text('${Provider.of<Counter>(builderContext).count}');
            }),
          ),
        ),
      ),
    );
  }
}

改法 2: 將 Provider 提至頂層:

void main() {
  runApp(
    Provider(
      create: (_) => Counter(),
      child: const MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(child: Text('${Provider.of<Counter>(context).count}')),
      ),
    );
  }
}

Get 由于全局單例帶來的問題

正如前面提到 Get 通過全局單例,默認以 runtimeType 為 key 進行對象的存儲,部分場景可能獲取到的對象不符合預期,例如商品詳情頁之間跳轉。由于不同的詳情頁實例對應的是同一 Class,即 runtimeType 相同。如果不添加 tag 參數,在某個頁面調用 Get.find 會獲取到其它頁面已經存儲過的對象。同時 Get 中一定要注意考慮到對象的回收,不然很有可能引起內存泄漏。要么手動在頁面 dispose 的時候做 delete 操作,要么完全使用 Get 中提供的組件,例如 GetBuilder,它會在 dispose 中釋放。

GetBuilder 中在 dispose 階段進行回收:

@override
void dispose() {
  super.dispose();
  widget.dispose?.call(this);
  if (_isCreator! || widget.assignId) {
    if (widget.autoRemove && GetInstance().isRegistered<T>(tag: widget.tag)) {
      GetInstance().delete<T>(tag: widget.tag);
    }
  }

  _remove?.call();

  controller = null;
  _isCreator = null;
  _remove = null;
  _filter = null;
}

Get 與 Provider 優缺點總結

通過本文,我向大家介紹了狀態管理的必要性、它解決了 Flutter 開發中的哪些問題以及是如何解決的,與此同時,我也為大家總結了在實踐中常見的問題等,看到這里你可能還會有些疑惑,到底是否需要使用狀態管理?

在我看來,框架是為了解決問題而存在。所以這取決于你是否也在經歷一開始提出的那些問題。如果有,那么你可以嘗試使用狀態管理解決;如果沒有,則沒必要過度設計,為了使用而使用。

其次,如果使用狀態管理,那么 Get 和 Provider 哪個更好?

這兩個框架各有優缺點,我認為如果你或者你的團隊剛接觸 Flutter,使用 Provider 能幫助你們更快理解 Flutter 的核心機制。而如果已經對 Flutter 的原理有了解,Get 豐富的功能和簡潔的 API,則能幫助你很好地提高開發效率。

感謝社區成員 Alex、Luke、Lynn、Ming 對本文的貢獻。


https://mp.weixin.qq.com/s/3mcY39i7DqZWivIrIFc8Yg

最多閱讀

如何有效定位Flutter內存問題? 1年以前  |  11672次閱讀
Flutter的手勢GestureDetector分析詳解 3年以前  |  7603次閱讀
Flutter插件詳解及其發布插件 2年以前  |  6289次閱讀
在Flutter中添加資源和圖片 3年以前  |  5140次閱讀
Flutter 狀態管理指南之 Provider 3年以前  |  4340次閱讀
發布Flutter開發的iOS程序 3年以前  |  4315次閱讀
Flutter for Web詳細介紹 3年以前  |  4188次閱讀
在Flutter中發起HTTP網絡請求 3年以前  |  3950次閱讀
使用Inspector檢查用戶界面 3年以前  |  3882次閱讀
Flutter Widget框架概述 3年以前  |  3257次閱讀
Flutter路由詳解 3年以前  |  3100次閱讀
JSON和序列化 3年以前  |  3025次閱讀
Flutter框架概覽 3年以前  |  2989次閱讀
推薦5個Flutter重磅開源項目! 1年以前  |  2949次閱讀
為Flutter應用程序添加交互 3年以前  |  2940次閱讀
使用自定義字體 3年以前  |  2842次閱讀
處理文本輸入 3年以前  |  2841次閱讀
編寫國際化Flutter App 3年以前  |  2830次閱讀

手機掃碼閱讀
18禁止午夜福利体验区,人与动人物xxxx毛片人与狍,色男人窝网站聚色窝
<蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <文本链> <文本链> <文本链> <文本链> <文本链> <文本链>