這是我參與8月更文挑戰的第10天,活動詳情檢視:8月更文挑戰
前言
之前的幾篇我們寫了狀態管理的機制和狀態管理外掛,接下來幾篇我們就使用官方推薦的 Provider 來改造舊的程式碼,你會發現改造前後具有十分大的差別。關於 Provider
的示例,之前翻譯了一篇官網推薦的購物車示例文章:Flutter 入門與實戰(四十):以購物車為例初探狀態管理。
Provider 簡介
Provider 當前最新版本是5.0.0,使得元件樹能夠共享狀態資料的方式為:
Provider (
create: (_) => Model(),
child: someWidget(),
);
複製程式碼
Provider
類本身並不會在狀態改變的時候自動更新子元件,因此更常用的是使用其子類:
ListenableProvider
:監聽實現了Listenable
的的物件,並將其暴露給下級元件。當觸發一個事件後會通知子元件依賴發生變化進而實現重建。ChangeNotifierProvider
:最為常用的一個方式,是ListenableProvider
的子類。監聽實現了ChangeNotifier
介面的物件,當該物件呼叫notifyListeners
的時候,就會通知全部的監聽元件更新元件。ValueListenableProvider
:監聽實現了ValueListenable
介面的物件。當該物件改變時,會更新其下級元件。StreamProvider
:監聽 Stream 物件,然後將其內容暴露給子元件。通常是向一個元件以流的方式提供大量的內容,例如電池電量監測、Firebase 查詢等。
如果一個物件被多個元件共享,那麼可以使用如下方式:
// 被多個元件共享的物件
MyChangeNotifier variable;
ChangeNotifierProvider.value(
value: variable,
child: ...
)
複製程式碼
在 Widget 中使用狀態資料有三種方式:
- 使用
context.read<T>()
方法:該方法返回T 型別的狀態資料物件,但不會監聽該物件的改變,適用於只讀的情況; - 使用
context.watch<T>()
方法:該方法返回 T 型別狀態資料物件,並且會監聽它的變化,適用於需要根據狀態更新的狀況。 - 使用
context.select<T,R>(R cb(T value))
方法:返回 T物件中的 R 型別物件,這可以使得Widget
只監聽狀態物件的部分資料。
詳細內容建議大家去看 Provider 的官方文件,我們後續的篇章也會涉及其中的內容。
程式碼分析
我們在前面的篇章介紹了一個動態模組的管理,包括了整個 CRUD 過程。具體可以從專欄:Flutter 入門與實戰的第二十二到第二十七篇。首先我們來改造一下列表的程式碼,回頭再來看之前的程式碼,就會知道為什麼說直接使用 setState 的方式更新介面的開發者會被評為“**草包**
”了!
之前程式碼一看就很亂,首先是在列表裡包括了新增、編輯、刪除的回撥程式碼,是想要是業務複雜一點,豈不是回撥要滿屏飛了!其次是業務程式碼和 UI 程式碼混用,一個是程式碼又臭又長——俗稱?一樣的程式碼,另外一個是業務程式碼的複用性降低了。比如說,我們在別的地方可能也會用到動態的增改刪查業務,總不能再複製、貼上再來一遍吧?
程式碼改造
現在我們來使用Provider
將業務和 UI 分離。將業務相關的程式碼統一放到狀態管理中,UI 這邊只處理介面相關的程式碼。首先抽取一個 DynamicModel
類,檔名為 dynamic_model.dart
,把列表的相關業務程式碼放進來:
- 列表資料:使用一個
List<DynamicEntity>
物件儲存列表資料,預設為空陣列。 - 分頁資料:當前頁碼
_currentPage
,固定每頁大小為20。 - 重新整理方法:
refresh
,將當前頁碼置為1,重新請求第一頁資料。 - 載入方法:
load
,將當前頁碼加1,請求第 N 頁的資料。 - 獲取分頁資料:根據當前頁面和分頁大小請求動態資料,並更新列表資料。
- 預留
delete
、add
和update
方法,以便後面的刪除、新增和更新使用。
整個DynamicModel
類的程式碼如下,這裡關鍵的一點是使用 with ChangeNotifier
使得 DynamicModel
混入ChangeNotifer
的特性,以便 ChangeNotifierProvider
能夠為其新增監聽器,並且在呼叫 notiferListeners
的時候通知狀態依賴的子元件進行更新。
class DynamicModel with ChangeNotifier {
List<DynamicEntity> _dynamics = [];
int _currentPage = 1;
final int _pageSize = 20;
List<DynamicEntity> get dynamics => _dynamics;
void refresh() {
_currentPage = 1;
_requestNewItems();
}
void load() {
_currentPage += 1;
_requestNewItems();
}
void _requestNewItems() async {
var response = await DynamicService.list(_currentPage, _pageSize);
if (response != null && response.statusCode == 200) {
List<dynamic> _jsonItems = response.data;
List<DynamicEntity> _newItems =
_jsonItems.map((json) => DynamicEntity.fromJson(json)).toList();
if (_currentPage == 1) {
_dynamics = _newItems;
} else {
_dynamics += _newItems;
}
}
notifyListeners();
}
void removeWithId(String id) {}
void add(DynamicEntity newDynamic) {}
void update() {}
}
複製程式碼
接下來是使用 Provider
為動態模組提供狀態管理,如前面的幾章所述,Provider
需要處於元件的上級才能夠為子元件提供狀態共享,因此我們有兩種方式來實現這種方式。
- 在構建
DynamicPage
列表頁面的app.dart
中將DynamicPage
作為Provider
的下級。如下所示,這種方式的缺點是因為這是首頁,如果各個模組的程式碼都往這裡對方,會使得 app.dart 很臃腫,而且耦合度也變高。
@override
void initState() {
super.initState();
_homeWidgets = [
ChangeNotifierProvider<DynamicModel>(
create: (context) => DynamicModel(),
child: DynamicPage(),
),
MessagePage(),
CategoryPage(),
MineSliverPage(),
];
}
複製程式碼
- 使用一個
Widget
包裹DynamicPage
以及Provider
來降低程式碼的耦合度,避免app.dart
中的程式碼過於臃腫。
class DynamicWrapper extends StatelessWidget {
const DynamicWrapper({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => DynamicModel(),
child: DynamicPage(),
);
}
}
複製程式碼
之後就是對 DynamicPage
進行改造,首先是將 DynamicPage
由 StatefulWidget
改為 StatelessWidget
,然後移除掉相關業務程式碼。,最後就是在 build
方法中從 Provider
獲取介面所需的資料,或呼叫對應的方法。改造完的 DynamicPage 就十分清爽了,如下所示:
class DynamicPage extends StatelessWidget {
DynamicPage({Key key}) : super(key: key);
final EasyRefreshController _refreshController = EasyRefreshController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('動態', style: Theme.of(context).textTheme.headline4),
actions: [
IconButton(
icon: Icon(Icons.add),
onPressed: () {
RouterManager.router
.navigateTo(context, RouterManager.dynamicAddPath);
}),
],
brightness: Brightness.dark,
),
body: EasyRefresh(
controller: _refreshController,
firstRefresh: true,
onRefresh: () async {
context.read<DynamicModel>().refresh();
},
onLoad: () async {
context.read<DynamicModel>().load();
},
child: ListView.builder(
itemCount: context.watch<DynamicModel>().dynamics.length,
itemBuilder: (context, index) {
return DynamicItem(context.watch<DynamicModel>().dynamics[index],
(String id) {
context.read<DynamicModel>().removeWithId(id);
});
},
),
),
);
}
}
複製程式碼
在 ListView.builder
中我們使用了 contxt.watch<DynamicModel>
方法來獲取最新的動態列表 ,從而使得當列表資料改變時能夠重新整理介面。而在呼叫方法方面,我們則使用了 context.read<DynamicModel>
方法,因為這裡並不需要監聽狀態的改變。執行一下,發現和之前的效果一樣,改造完成。
改造前後對比
我們來對比改造前後的 DynamicPage
程式碼,如下圖所示(左側為舊程式碼)。可以看到,大部分程式碼都被移除了,實際原先的程式碼有120
行,而現在的程式碼只有40
行了,足足減少了2/3!
當然,程式碼減少是因為將業務程式碼抽離了,但是業務程式碼本身是可以複用的。下一篇我們將刪除、新增和編輯完成後,再來看 Provider
如何進一步提高程式碼複用性和簡化頁面程式碼。
總結
通過 Provider 狀態管理,得到的最大的好處其實是 UI 層和業務層程式碼分離,精簡了 UI層程式碼的同時,也提高了業務程式碼的複用性。而 Provider 的區域性重新整理特性,也能夠提高介面渲染的的效能。
我是島上碼農,微信公眾號同名,這是Flutter 入門與實戰的專欄文章。
??:覺得有收穫請點個贊鼓勵一下!
?:收藏文章,方便回看哦!
?:評論交流,互相進步!