這是我參與8月更文挑戰的第17天,活動詳情檢視:8月更文挑戰
前言
在有些場景中,我們的元件只和狀態管理的部分資料有關。舉個例子,例如下面的動態詳情介面。 底部有兩個按鈕:
- 點贊按鈕:點選按鈕後會增加點贊數,再次點贊會取消點贊並減少點贊數;
- 收藏按鈕:點選按鈕增加收藏數,再次點選會取消收藏並減少收藏數。
這個情況,如果我們使用 Provider
的 watch
方法監聽狀態物件的變化時,每次點選任意一個按鈕都會導致整個介面重新整理,這樣不可避免會帶來效能上的損耗。那有沒有辦法實現介面只監聽與它有關的資料呢?本篇我們就來講述如何實現監聽狀態的區域性變化。
Provider 狀態區域性監聽
Provider
提供了方法 R select<T, R>(R Function(T value) selector)
來實現狀態資料的區域性變化,該方法接收一個 selector
函式引數,該函式的引數為 T
,然後返回 R
型別值。也就是隻監聽狀態資料的 R
部分資料,而不是監聽全部資料。只有當 R
型別值發生改變時,才會通知依賴該資料的元件重新整理,而其他不依賴該值的元件不會重新整理。
詳情頁改造
我們新建一個 DynamicDetailModel 類作為詳情頁的狀態管理,其中有三個狀態資料:
- _currentDynamic:當前動態詳情資料,提供 get 屬性供詳情元件訪問;
- _praiseCount:點贊數,提供 get 屬性供點贊按鈕元件訪問;
- _favorCount:收藏數,提供 get 屬性供收藏按鈕元件訪問;
同時增加了更新點贊數的方法 updatePraiseCount 和 更新收藏數方法 updateFavorCount,兩個方法的邏輯如下:
- updatePraiseCount:如果沒有點贊則點贊數+1,如果點讚了,則點贊數-1。
- updateFavorCount:如果沒有收藏則收藏數+1,如果收藏了,則收藏數-1。
詳情頁面我們分成了三個大的元件,使用 DynamicDetailWrapper
包裹,以便三個元件給自管理自己的狀態,其中兩個按鈕使用 Stack + Positioned
元件將按鈕固定在頁面底部。
class DynamicDetailWrapper extends StatelessWidget {
final String id;
const DynamicDetailWrapper({Key? key, required this.id}) : super(key: key);
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<DynamicDetailModel>(
create: (context) => DynamicDetailModel(),
child: Stack(
children: [
_DynamicDetailPage(id),
Positioned(
bottom: 0,
height: 60,
width: MediaQuery.of(context).size.width,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_PraiseButton(),
_FavorButton(),
],
))
],
));
}
}
複製程式碼
接下來就是三個元件各自使用 select
方法監聽狀態的區域性資料。
// _DynamicDetailPage
Widget build(BuildContext context) {
print('_DynamicDetailPage');
DynamicEntity? currentDynamic =
context.select<DynamicDetailModel, DynamicEntity?>(
(dynamicDetail) => dynamicDetail.currentDynamic,
);
return Scaffold(
appBar: AppBar(
title: Text('動態詳情'),
brightness: Brightness.dark,
),
body: currentDynamic == null
? Center(
child: Text('請稍候...'),
)
: _getDetailWidget(currentDynamic),
);
}
// ...省略其他程式碼
}
// _PraiseButton
class _PraiseButton extends StatelessWidget {
const _PraiseButton({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
print('PraiseButton');
return Container(
alignment: Alignment.center,
color: Colors.blue,
child: TextButton(
onPressed: () {
context.read<DynamicDetailModel>().updatePraiseCount();
},
child: Text(
context.select<DynamicDetailModel, String>(
(DynamicDetailModel dyanmicDetail) {
return '點贊 ' + dyanmicDetail.praiseCount.toString();
},
),
style: TextStyle(color: Colors.white),
),
style: ButtonStyle(
minimumSize: MaterialStateProperty.resolveWith((states) =>
Size((MediaQuery.of(context).size.width / 2 - 1), 60))),
),
);
}
}
// _FavorButton 和_PraiseButton 類似,省略
複製程式碼
三個元件對依賴的物件全部使用了 select
方法替換了 watch
方法,並且每個元件都在 build
方法列印了各自的元件名稱,以便驗證是否只在其依賴的那部分狀態資料發生改變時才重新整理。
這裡順帶說一個小坑,點贊按鈕和收藏按鈕一開始設定的寬度都是
MediaQuery.of(context).size.width / 2
,結果點選收藏按鈕時會同時點選到點贊按鈕,因此將其中一個按鈕的寬度減了1(任意一個按鈕都行),這應該是浮點數精度問題,導致兩個按鈕區域覆蓋了,結果同時觸發了兩個按鈕的點選事件。
執行結果
現在我們來看執行結果,對照模擬器介面操作和控制檯日誌,可以看到,點選任意一個按鈕時只有按鈕自身改變,其他不相關的元件沒有呼叫 build
方法。
從控制檯列印的結果來看,點選按鈕只會呼叫該按鈕的 build 方法,其他元件沒有重新 build,確實實現了區域性重新整理的效果。
總結
本篇通過把動態詳情頁分離為三個依賴狀態不同資料的三個元件,使用 Provider
提供的 select
方法實現了狀態區域性監聽。通過這個例子,其實給了我們狀態管理的推薦做法,那就是儘量優先使用 select
方法做區域性監聽,這樣在日後狀態管理擴充套件後也能做到狀態管理不同部分的介面更新分離,實現資料只驅動關聯介面重新整理,從而取得更好的效能。
我是島上碼農,微信公眾號同名,這是Flutter 入門與實戰的專欄文章,對應原始碼請看這裡:Flutter 入門與實戰專欄原始碼。
??:覺得有收穫請點個贊鼓勵一下!
?:收藏文章,方便回看哦!
?:評論交流,互相進步!