Flutter入門與實戰(四十一): 從InheritedWidget深入瞭解狀態管理機制(上)

島上碼農發表於2021-08-04

這是我參與8月更文挑戰的第4天,活動詳情檢視:8月更文挑戰

本文翻譯自 Flutter 官方推薦的文章:Managing Flutter Application State With InheritedWidgets。通過官網文件或推薦文章,能夠讓我們更好地瞭解 Flutter 的狀態管理機制。

前言

通常來說,互動式應用可以分為三個部分:ModelViewController,也就是我們常說的MVC 模式。使用過Flutter樣例的人會對使用Widget和回撥方式來構建檢視和控制器的響應式方式很熟悉。但是,對於 Model 這一層來說,確未必那麼清晰。Flutter 的 Model 層實際代表了其保持的狀態。Widget 為狀態提供了視覺化的呈現,並且允許使用者修改它。 當widgetbuild方法從 Model 中獲取值時,或者回撥函式修改 Model 值的時候,widget將會隨著 Model 的改變而重新構建。本篇文章就是介紹這一切是怎麼發生的。 本篇文章回顧了 Flutter 的有狀態元件和 InheritedWidget 類如何將應用的視覺化元素繫結到 Model 上。並且引入了一個可以輕鬆植入應用的ModelBinding類。

宣告

本篇介紹的構建 MVC 應用的方式並不是唯一的。如果你要構建大型的應用的話,有很多種方式可以將 Flutter 繫結到模型上。本篇結尾也會列出其中的一些。換言之,即便你決定最後不使用 ModelBinding 類,你也可以收穫到 Flutter 的狀態管理機制。

這並不是面向初學者的文章,你至少需要對 Flutter 的 API 有一定的瞭解,例如:

  • 你能夠熟練地使用 Dart 編寫類,並且瞭解==操作符和雜湊碼過載,以及泛型方法。
  • 對基本的 Flutter 元件類熟悉,並且懂得如何自己寫一個新的元件。

應用的模型

為了展示本篇的示例,我們需要一個樣例應用模型。為了聚焦,我們會將這個模型設計得儘可能地簡單。在我們的模型類中只有一個值,以及包括了操作符==hashCode 過載。

class ViewModel {
  const ViewModel({ this.value = 0 });

  final int value;

  @override
  bool operator ==(Object other) {
    if (identical(this, other))
      return true;
    if (other.runtimeType != runtimeType)
      return false;
    final ViewModel otherModel = other;
    return otherModel.value == value;
  }

  @override
  int get hashCode => value.hashCode;

  static ViewModel of(BuildContext context) {
    final ModelBinding binding = context.dependOnInheritedWidgetOfExactType(aspect: ModelBinding);
    return binding.model;
  }
}
複製程式碼

當然,這個模型也可以根據應用的實際情況進行擴充套件。 注意,這是一個不可變的模型,因此如果要改變的只能是替換它。下面的MVC方式也可以用可變的模型,但是那樣會稍微有點複雜。

將模型與有狀態元件繫結

這是整合模型最簡單的方式,對於只需要一個下午就搞定的應用來說非常合適。 有狀態元件會與一個保持狀態的 State 物件關聯。這個 State 物件會的 build 的方法會構建該元件的子元件樹,就像無狀態元件的build 方法一樣。呼叫State 物件的 setState 方法時,在間隔一個顯示楨切換的時間間隔後,將會觸發元件重新構建。如果有狀態元件的狀態物件持有模型,那麼用於配置它的build方法在呼叫 setState 方法時,就會使用模型的值。 下面的這個有狀態元件十分簡單,只是持有了一個 ViewModel 物件,然後提供了一個 update 方法來更新模型。

class ViewController extends StatefulWidget {
  _ViewControllerState createState() => _ViewControllerState();
}

class _ViewControllerState extends State<ViewController> {
  Model currentModel = ViewModel();
  
  void updateModel(ViewModel newModel) {
    if (newModel != currentModel) {
      setState(() {
        currentModel = newModel;
      });
    }
  }
  
  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () {
        updateModel(ViewModel(value: currentModel.value + 1));
      },
      child: Text('Hello World ${currentModel.value}),
    );
  }
}
複製程式碼

使用有狀態元件繫結的限制

使用上面的方式寫程式碼的肯定是個草包(?這話不是我說的,誤傷了請自醒——我們之前的示例程式碼也是這麼寫的,確實很初級)。對於相對大規模的應用來說,就很不適用了。具體來說,會有如下的缺陷:

  • 當模型改變的時候,整個 ViewController 以及整個元件樹都會被重建,而不僅僅是依賴模型的那個元件(即 Text)。
  • 如果子元件需要使用模型的值,模型只能通過構造器引數傳遞,或者使用回撥閉包。
  • 如果再往下層級的元件需要使用模型的值,那隻能是沿著元件樹的鏈條一層層往下傳遞模型物件。
  • 如果子元件需要修改模型的值,那就必須呼叫 ViewController 傳遞的回撥函式。這個例子中就如同 RaisedButtononPressed 方法那樣。

因此,有狀態元件其實更適用於建立自己內部的狀態,而不適用於在複雜應用中共享資料模型。而對應程式設計師來說,搞定複雜的事情才讓我們顯得更有價值

版本0:使用 InheritedWidget 繫結模型

InheritedWidget 類有一些特殊的使得它很適合在元件樹裡共享模型。

  • 給定一個 BuildContext,查詢一個特定型別的最近的InheritedWidget 的祖先節點十分便捷,只需要按表查詢就行。
  • InheritedWidget 會跟蹤他們的依賴,例如用於訪問InheritedWidgetBuildContext。當一個 InheritedWidget 重建時,所有它依賴的物件都會被重建。

實際上你很可能已經接觸過InheritedWidget 了,例如 Theme 這個元件。Theme.of(context)方法會返回 ThemeThemeData 物件,並且將 context 當做是Theme的一個依賴物件。如果 Theme 物件被重建,並且使用了不同的 ThemeData 值,那麼所有依賴於 Theme.of()的元件都會被自動重建。

使用自定義的 InheritedWidget 子類可以按相同的方式實現應用模型的宿主。這裡,我們把這個子類稱之為 ModelBinding,因為它將應用的元件和模型關聯在一起了。

class ModelBinding extends Inherited {
  ModelBinding({
    Key key,
    this.model = const ViewModel(),
    Widget child,
  }): assert(model != null), super(Key: key, child:child);
  
  final ViewModel model;
  
  @override
  bool updateShouldNotify(ModelBinding oldWidget) => model != oldWidget.model;
}
複製程式碼

updateShouldNotify 方法在 ModelBinding 被重建時會被呼叫。如果返回值是 true,那麼依賴它的全部元件都會被重建。

BuildContext inheritFromWidgetOfExactType()方法用於查詢一個 InheritedWidget 。由於這個方式有點醜,我們稍後再來介紹它。通常,這個方法是使用靜態方法包裹。將查詢方法加入到ViewModel能夠使得任何依賴 ModelBinding 物件的下級元件都可以通過 Model.of(context)方法獲取到 ViewModel 物件。

// 現在在 ModelBinding 的下級元件可以通過 Model.of(context)訪問了
Text('Hello WOrld ${ViewModel.of(context).value}')
複製程式碼

任何 ModelBinding 的下級都可以這麼做,而無需一層層傳遞 ViewModel 物件了。如果ViewModel 物件發生了改變,下級元件就像 ViewController 一樣自動被重建。

ModelBinding 所在的元件自身必須是有狀態元件。為了更改 ViewModel 物件,該元件還是需要呼叫 setState 方法。這裡我們使用了一個 StateViewController 有狀態元件來持有模型物件,而更新Model 物件的方法被當做回撥函式傳遞給了 ViewController

class StateViewController extends StatefulWidget {
  StateViewController({Key key}) : super(key: key);

  @override
  _StateViewControllerState createState() => _StateViewControllerState();
}

class _StateViewControllerState extends State<StateViewController> {
  ViewModel currentModel = ViewModel();

  void _updateModel(ViewModel newModel) {
    setState(() {
      currentModel = newModel;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('模型繫結版本0'),
      ),
      body: Center(
        child: ModelBinding(
          model: currentModel,
          child: ViewController(_updateModel),
        ),
      ),
    );
  }
}
複製程式碼

這種情況下,ViewModel 類是簡單的不可變物件,因此只需要賦值一個新的ViewModel物件替換即可完成更新。替換該物件也可能更復雜,例如如果物件引用的 物件需要進行生命週期管理,那麼替換該模型的時候可能會需要銷燬部分舊的物件。

這個版本的缺陷

執行結果就不貼圖了,就是點選按鈕數字自動加1,程式碼已提交至:Flutter 狀態管理程式碼。這個版本從好的方面來看,這個版本的 ModelBinding 類使得元件很容易獲取模型物件,並且當模型改變的時候可以自動重建。

但是,反過來,這個版本還需要使用 updateModel 回撥方法沿著元件樹傳遞到實際控制狀態改變的元件,這種方式的程式碼並不好維護。下一個版本我們來實現一個更通用的 ModelBinding 類,是的子元件可以直接通過ModelBinding提供的 update 方法更新ViewModel 物件。

總結

本篇介紹了 Flutter 應用中的 MVC 模型,對於 Flutter 而言,應用中模型實際上就是元件的狀態。如果直接通過一層層的狀態傳遞去控制元件樹的下級元件的顯示,將會導致程式碼耦合嚴重。因此,本篇引入了一個 ModelBinding 類,通過繼承 InheritedWidget來實現子元件可以直接訪問上級元件的狀態,從而避免了狀態引數的 層層傳遞。當然,這個版本還存在一個缺陷,那就是更改狀態的回撥方法還是需要沿著元件樹傳遞,這個我們在下篇會改造一個更通用的 ModelBinding 類。


我是島上碼農,微信公眾號同名,這是Flutter 入門與實戰的專欄文章。

??:覺得有收穫請點個贊鼓勵一下!

?:收藏文章,方便回看哦!

?:評論交流,互相進步!

相關文章