一、ScopedModel簡介
ScopedModel
屬於入門級別的狀態管理框架,它的思想比較簡單,參考官方文件便可以很容易理解其中構架。
在Flutter
中Lifting state up
(狀態提升)是十分必要的,狀態提升可以理解為把元件之間相互共享的狀態提取出來放在一個較高層級中管理的一種思想。ScopedModel
提供了對於這種狀態管理的便利。
二、ScopedModel中的三個概念
ScopedModel
主要有三個重要的概念,也是其中的三個類:Model
、ScopedModel
和ScopedModelDescendant
。ScopedModel
基本上通過這三個類實現其功能。
Model
是封裝狀態和狀態操作的地方。我們可以將想要的資料存放在Model
當中並且將對資料操作,如新增刪除的相關方法放在這裡。Model
還提供了一個notifyListeners()
方法,它的作用是當資料發生改變時,可以通過呼叫notifyListeners()
方法通知介面進行更新。
ScopedModel
是一個用於儲存Model
的Widget
。通常ScopedModel
會一個應用的入口處作為父佈局使用,並以Model
作為引數傳入,使得ScopedModel
持有Model
。
在ScopedModel
的子佈局中,可以通過ScopedModel.of<Model>(context)
方法來獲取Model
。
ScopedModelDescendant
,顧名思義,是ScopedModel
的派生物。同樣的,它也是一個Widget
。ScopedModelDescendant
會作為ScopedModel
下的子佈局存在,它的主要作用是響應狀態更新。
ScopedModelDescendant
中存在builder
函式,這個函式會在Model
的notifyListeners()
發生時被呼叫,從而根據Model
中的資料生成相應的介面。
三、ScopedModel的實踐
這裡以常見的獲取列表選擇列表為例子。一個頁面用於展示選中項和跳轉到列表,一個頁面用於顯示列表。
1. 引入scoped_model
第三方庫
在根目錄的pubspec.yaml
檔案的dependencies
中加入依賴
dependencies:
...
scoped_model: ^1.0.0
複製程式碼
2. 定義Model
建立一個ListModel
類,這個類需要繼承scoped_model
包裡的Model
類。
ListModel
類中包含三個狀態:列表初始化標誌、列表資料、選中的列表項。
bool _init = false; // 列表初始化標誌
List<String> _list = []; // 列表資料
String _selected = '未選中'; // 選中的列表項
複製程式碼
在Model
中不僅只有資料,還包括對資料操作的方法,這裡定義兩個操作方法,分別是選中列表專案和載入列表的方法,並且,這兩個方法在更新資料後,需要呼叫notifyListeners()
通知UI更新。
/**
* 選中列表項
*/
void select(String selected) {
_selected = selected;
// 通知資料變更
notifyListeners();
}
/**
* 載入列表
*/
void loadList() async {
// 模擬網路請求
await Future.delayed(Duration(milliseconds: 3000));
_list = [
'1. Scoped Model',
'2. Scoped Model',
'3. Scoped Model',
'4. Scoped Model',
'5. Scoped Model',
'6. Scoped Model',
'7. Scoped Model',
'8. Scoped Model',
'9. Scoped Model',
'10. Scoped Model'
];
_init = true;
// 通知資料變更
notifyListeners();
}
複製程式碼
3. UI佈局
在UI上,使用ScopedModel
作為根佈局,提供Model
,使用ScopedModelDescendant
作為子佈局,響應Model
。
首先,在main()
方法中,建立ListModel
例項,用ScopedModel
包裹MyApp佈局
void main() {
// 建立Model例項
ListModel listModel = ListModel();
// 使用ScopedModel作為根佈局
runApp(ScopedModel(model: listModel, child: MyApp()));
}
複製程式碼
為了體現狀態提升這一概念,例子中使用兩個頁面,一個是ShowPage
,另一個是ListPage
。ShowPage
用於顯示選中的列表專案和提供跳轉到ListPage
的入口,ListPage
用於載入顯示列表。
在ShowPage
中,顯示ListModel
中的選中項。
ScopedModelDescendant<ListModel>(
builder: (context, child, model) {
String selected = model.selected;
return Text(selected);
}
),
複製程式碼
ScopedModelDescendant
的泛型指明ListModel
,它便會自動獲取ScopedModel
中的ListModel
,在builder: (context, child, model)
中即可通過其中的model
引數獲取狀態,構建UI。
同樣的,在ListPage
中,通過ScopedModelDescendant
來顯示載入狀態和列表。
body: ScopedModelDescendant<ListModel>(builder: (context, child, model) {
// 根據狀態顯示介面
if (!model.isInit) {
// 顯示loading介面
return buildLoad();
} else {
// 顯示列表介面
var list = model.list;
return buildList(list);
}
}),
複製程式碼
4. 狀態改變
ListPage
是一個StatefulWidget
,所以可以在initState()
方法中進行列表的載入工作。
@override
void initState() {
super.initState();
ListModel model = ScopedModel.of<ListModel>(context); // 獲取ListModel
if (!model.isInit) {
model.loadList(); // 載入列表
}
}
複製程式碼
點選列表中的某一項時,會選中該項,這時呼叫ListModel
中的select(String)
方法,並返回上一個介面。
onTap: () {
ListModel model = ScopedModel.of<ListModel>(context);
model.select(list[index]);
// 返回到上一級頁面
Navigator.pop(context);
},
複製程式碼
完整程式碼可以參考github.com/windinwork/…
四、ScopedModel的注意事項
ScopedModelDescendant
的層級需要儘量低,可以避免大範圍的UI重建。這裡引用官方的例子。
// 錯誤示例
return ScopedModelDescendant<CartModel>(
builder: (context, child, cart) {
return HumongousWidget(
// ...
child: AnotherMonstrousWidget(
// ...
child: Text('Total price: ${cart.totalPrice}'),
),
);
},
);
複製程式碼
// 正確示例
return HumongousWidget(
// ...
child: AnotherMonstrousWidget(
// ...
child: ScopedModelDescendant<CartModel>(
builder: (context, child, cart) {
return Text('Total price: ${cart.totalPrice}');
},
),
),
);
複製程式碼
五、總結
ScopedModel
功能比較簡單,使用Model
儲存狀態和通知狀態改變,使用ScopedModel
提供Model
,使用ScopedModelDescendant
佈局來響應狀態變化,是一個十分適合入門者理解的狀態管理模型。