這是我參與8月更文挑戰的第4天,活動詳情檢視:8月更文挑戰
本文翻譯自 Flutter 官方推薦的文章:Managing Flutter Application State With InheritedWidgets。通過官網文件或推薦文章,能夠讓我們更好地瞭解 Flutter 的狀態管理機制。
前言
通常來說,互動式應用可以分為三個部分:Model
,View
和 Controller
,也就是我們常說的MVC 模式。使用過Flutter樣例的人會對使用Widget
和回撥方式來構建檢視和控制器的響應式方式很熟悉。但是,對於 Model
這一層來說,確未必那麼清晰。Flutter 的 Model
層實際代表了其保持的狀態。Widget 為狀態提供了視覺化的呈現,並且允許使用者修改它。
當widget
的build
方法從 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
傳遞的回撥函式。這個例子中就如同RaisedButton
的onPressed
方法那樣。
因此,有狀態元件其實更適用於建立自己內部的狀態,而不適用於在複雜應用中共享資料模型。而對應程式設計師來說,搞定複雜的事情才讓我們顯得更有價值。
版本0:使用 InheritedWidget 繫結模型
InheritedWidget
類有一些特殊的使得它很適合在元件樹裡共享模型。
- 給定一個
BuildContext
,查詢一個特定型別的最近的InheritedWidget
的祖先節點十分便捷,只需要按表查詢就行。 InheritedWidget
會跟蹤他們的依賴,例如用於訪問InheritedWidget
的BuildContext
。當一個InheritedWidget
重建時,所有它依賴的物件都會被重建。
實際上你很可能已經接觸過InheritedWidget
了,例如 Theme
這個元件。Theme.of(context)
方法會返回 Theme
的 ThemeData
物件,並且將 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 入門與實戰的專欄文章。
??:覺得有收穫請點個贊鼓勵一下!
?:收藏文章,方便回看哦!
?:評論交流,互相進步!