前言
我們通過一個實(shí)際頁面來使用并且理解一下Flutter Provider
的使用坏挠,了解下Provider
如果進(jìn)行狀態(tài)存儲以及共享
頁面
微信圖片_20220604131605.jpg
|
微信圖片_20220604131740.jpg
|
InheritedWidget
InheritedWidget
是一個功能性的組件扫外,可以在組件樹種從上往下的進(jìn)行數(shù)據(jù)共享初澎,定義在InheritedWidget
組件中的數(shù)據(jù)可以被其子節(jié)點(diǎn)獲取到抄谐,并且當(dāng)InheritedWidget
刷新時,所以依賴它的子節(jié)點(diǎn)都會進(jìn)行刷新
創(chuàng)建一個需要共享的數(shù)據(jù)類
class ShareData{
bool isEnableBiometric;
void setIsEnableBiometric(bool isEnableBiometric){
this.isEnableBiometric = isEnableBiometric;
}
ShareData(this.isEnableBiometric);
}
創(chuàng)建一個InheritedWidget
里面包含需要被共享的數(shù)據(jù)
class ShareWidget extends InheritedWidget {
ShareData shareData;
static ShareWidget? of(BuildContext context) {
//會將調(diào)用該方法的Widget進(jìn)行注冊晾腔,當(dāng)數(shù)據(jù)刷新時咋會對其進(jìn)行刷新,所注冊組件的會調(diào)用didChangeDependencies -> build
return context.dependOnInheritedWidgetOfExactType<ShareWidget>();
}
static ShareData? ofValue(BuildContext context) {
//會將調(diào)用該方法的Widget不會進(jìn)行注冊
return context.findAncestorWidgetOfExactType<ShareWidget>()?.shareData;
}
ShareWidget({required this.shareData, Key? key, required Widget child})
: super(key: key, child: child);
@override
bool updateShouldNotify(covariant InheritedWidget oldWidget) {
///框架是否通知繼承于這個組件并注冊的子組件
return true;
}
}
static ShareWidget? of(BuildContext context)
由于子節(jié)點(diǎn)需要使用共享的數(shù)據(jù)憋活,所以需要暴露出函數(shù)讓子節(jié)點(diǎn)可以拿到父或者祖節(jié)點(diǎn)的InheritedWidget
從而拿到共享數(shù)據(jù),并且在調(diào)用findAncestorWidgetOfExactType
時子Widget
會進(jìn)行注冊,從而在日后InheritedWidget
變化時可以被刷新浦箱,當(dāng)組件樹中的InheritedWidget
更新時會通知所有已經(jīng)注冊的子組件進(jìn)行刷新狀態(tài)
updateShouldNotify
表示已經(jīng)注冊的子Widget
是否會在InheritedWidget
變化時刷新,當(dāng)子Widget
被通知刷新時會調(diào)用Widget
的didChangeDependencies
函數(shù)
在主頁面定義初始化共享數(shù)據(jù)
在主頁面使用InheritedWidget
class TestSharePageState extends State {
ShareData shareData = ShareData(false);
@override
void initState() {
super.initState();
Future.delayed(Duration(seconds: 5),(){
setState(() {
shareData.setIsEnableBiometric(true);
});
});
}
@override
Widget build(BuildContext context) {
return ShareWidget(shareData: shareData,
child: Column(
children: [
TestSharePage1(),
TestSharePage2(),
TestSharePage3(),
],
));
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
print('TestSharePageState didChangeDependencies');
}
}
在子頁面使用共享數(shù)據(jù)
class TestSharePage1State extends State {
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
child: Text(
'TestSharePage1${ShareWidget.of(context)?.shareData.isEnableBiometric ?? null}'),
);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
print('TestSharePage1State didChangeDependencies');
}
}
class TestSharePage2State extends State {
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
child: Text(
'TestSharePage2${ShareWidget.of(context)?.shareData.isEnableBiometric ?? null}'),
);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
print('TestSharePage2State didChangeDependencies');
}
}
class TestSharePage3State extends State {
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
child: Text('TestSharePage3 test'),
);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
print('TestSharePage3State didChangeDependencies');
}
}
刷新共享數(shù)據(jù)
主界面在5秒之后刷新了共享數(shù)據(jù)
@override
void initState() {
super.initState();
Future.delayed(Duration(seconds: 5),(){
setState(() {
shareData.setIsEnableBiometric(true);
});
});
}
五秒之后會打印剔应,并且Page1
和Page2
都會刷新助赞,Page3
由于沒有注冊所以不會刷新
I/flutter ( 3479): TestSharePage1State didChangeDependencies
I/flutter ( 3479): TestSharePage2State didChangeDependencies
只使用數(shù)據(jù)而不注冊
上述測試中,Page3
由于沒有使用共享數(shù)據(jù)非春,所以共享數(shù)據(jù)在刷新的時候Page3
沒有注冊所以沒有刷新柱徙,我們也可以只使用Page3
但是不注冊這樣就不會刷新了
使用下列方式可以只是使用而不進(jìn)行注冊
static ShareData? ofValue(BuildContext context) {
//會將調(diào)用該方法的Widget不會進(jìn)行注冊
return (context.getElementForInheritedWidgetOfExactType<ShareWidget>()?.widget as ShareWidget).shareData;
}
class TestSharePage3State extends State {
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
child: Text('TestSharePage3 ${ShareWidget.ofValue(context)?.isEnableBiometric ?? 'null'}}'),
);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
print('TestSharePage3State didChangeDependencies');
}
}
此時進(jìn)行數(shù)據(jù)刷新缓屠,則Page3不會調(diào)用didChangeDependencies
,但是數(shù)據(jù)依舊刷新了,因?yàn)楦腹?jié)點(diǎn)在重新構(gòu)建時护侮,子節(jié)點(diǎn)也會刷新
Provider
上面我們基本了解了InheritedWidget
是因?yàn)楦缸庸?jié)點(diǎn)的關(guān)系所以可以子啊父節(jié)點(diǎn)中存儲數(shù)據(jù)敌完,子節(jié)點(diǎn)中使用數(shù)據(jù)從而進(jìn)行數(shù)據(jù)共享以及局部頁面刷新等操作,Provider框架也是基于這樣的原理去構(gòu)建的一個更好用的全局狀態(tài)管理羊初,數(shù)據(jù)共享滨溉,局部刷新框架
我們來使用Provider框架去實(shí)現(xiàn)我們最開偷那兩張圖片的案例
首先我們添加provider
依賴
provider: ^4.0.4
簡單分析一下案例
案例中我們需要選擇多個興趣標(biāo)簽,并且會顯示你已經(jīng)選擇了幾個標(biāo)簽长赞,然后進(jìn)行提交晦攒,那么我們所共享的數(shù)據(jù)就是用戶所選擇的興趣標(biāo)簽,并且共享數(shù)據(jù)的范圍就是當(dāng)前頁面
共享數(shù)據(jù)的提供: 只是在當(dāng)前頁面的頂部對共享數(shù)據(jù)進(jìn)行提供以及初始化得哆,默認(rèn)選中的興趣標(biāo)簽數(shù)組數(shù)量為0
需要使用到共享數(shù)據(jù)的組件:
1.每個興趣的item
需要使用到共享數(shù)據(jù)用以判斷當(dāng)前item是否需要被選中(變色)
2.已選擇數(shù)量所展示的Text
3.底部的提交按鈕需要用到共享數(shù)據(jù)脯颜,但是只是使用而已
Provider的介紹
它是對InheritedWidget
的一個分封裝以及擴(kuò)展,提供了更好的性能以及更簡單的使用方式等
創(chuàng)建需要共享的數(shù)據(jù)類
class Topics {
var _chooseTopics = <String>[];
List<String> get chooseTopics => _chooseTopics;
void chooseTopic(String topic) {
if (!_chooseTopics.contains(topic)) {
_chooseTopics.add(topic);
} else {
_chooseTopics.remove(topic);
}
}
}
將共享數(shù)據(jù)通過Provider暴露
class ChooseTopicPage extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return ChooseTopicState();
}
}
class ChooseTopicState extends State {
Topics _topics = Topics();
@override
Widget build(BuildContext context) {
print('ChooseTopicState build');
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text('Topics', style: TextStyle(color: Colors.black)),
backgroundColor: Colors.white,
leading: Icon(Icons.arrow_back_ios, color: Colors.green)),
body: Provider<Topics>(
create: (_) => _topics,
child: Container(
padding: EdgeInsets.all(20),
child: Column(children: [
TopicWrap(),
Container(
height: 40,
decoration: BoxDecoration(
borderRadius: BorderRadius.all((Radius.circular(5))),
color: Colors.green),
child: MaterialButton(
textColor: Colors.white,
onPressed: () => {
},
child: Text('選擇'),
),
),
]),
),
));
}
}
這里的TopicWrap
是一個封裝的Widget
,在里面會使用到共享數(shù)據(jù)
class TopicWrapState extends State {
List<String> topics = [
"歐美電影",
"日本電影",
"日本動漫",
"大陸電影",
"恐怖電影",
"豆瓣Top250",
];
List<Widget> getWrapList() {
var childrens = <Widget>[];
for (var i = 0; i < topics.length; i++) {
childrens.add(GestureDetector(
child: Container(
padding: EdgeInsets.all(5),
margin: EdgeInsets.all(10),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(5)),
color: Provider
.of<Topics>(context).chooseTopics.contains(topics[i])
? Colors.orange
: Colors.green),
child: Text(
topics[i],
style: TextStyle(color: Colors.white),
),
),
onTap: () {
Provider.of<Topics>(context,listen: false).chooseTopic(topics[i]);
setState(() {
});
}));
}
return childrens;
}
@override
Widget build(BuildContext context) {
return Column(children: [
Wrap(children: getWrapList()),
Container(
margin: EdgeInsets.all(20),
child: Text('已選擇${Provider
.of<Topics>(context)
.chooseTopics
.length}'),
),
],);
}
}
這里使用Provider.of
獲取到了共享數(shù)據(jù)的長度
Provider.of<Topics>(context).chooseTopics.length
并且在點(diǎn)擊條目的時候也是使用同樣的方式獲取共享數(shù)據(jù)然后修改數(shù)據(jù)柳恐,這是使用listen: false
是因?yàn)檫@里并沒有組件使用并顯示數(shù)據(jù)伐脖,所以并不需要對其進(jìn)行注冊
Provider.of<Topics>(context,listen: false).chooseTopic(topics[i])
然后在修改數(shù)據(jù)之后使用了setState(() { })
進(jìn)行頁面刷新,然后就可以達(dá)到更新的效果
所以我們的結(jié)構(gòu)是:
這樣可以實(shí)現(xiàn)功能乐设,Provider
只是提供了數(shù)據(jù)存儲的功能讼庇,并且每次數(shù)據(jù)變化都個要刷新整個Wrap
頁面從而達(dá)到對應(yīng)的效果
ChangeNotifyProvider
使用ChangeNotifyProvider
,它會在數(shù)據(jù)更新之后通知所有注冊的Widget去進(jìn)行build,不用我們手動的去更新頁面
修改數(shù)據(jù)類
ChangeProvider
需要一個ChangeNotifier
類型的共享數(shù)據(jù)近尚,并且需要在數(shù)據(jù)被操作需要刷新時調(diào)用notifyListeners()
函數(shù)
class Topics extends ChangeNotifier {
var _chooseTopics = <String>[];
List<String> get chooseTopics => _chooseTopics;
void chooseTopic(String topic) {
if (!_chooseTopics.contains(topic)) {
_chooseTopics.add(topic);
} else {
_chooseTopics.remove(topic);
}
notifyListeners();
}
}
將Provider
修改為ChangeNotifyProvider
然后移除掉上述案例中的setState(() { })
蠕啄,還是可以達(dá)到剛才的效果
Consumer
在我們的案例中底部還有一個選擇的確認(rèn)按鈕,他需要拿到共享數(shù)據(jù)然后做一些其他的業(yè)務(wù)操作戈锻,我們給他加上獲取數(shù)據(jù)的代碼
@override
Widget build(BuildContext context) {
print('ChooseTopicState build');
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text('Topics', style: TextStyle(color: Colors.black)),
backgroundColor: Colors.white,
leading: Icon(Icons.arrow_back_ios, color: Colors.green)),
body: ChangeNotifierProvider<Topics>(
create: (_) => _topics,
child: Container(
padding: EdgeInsets.all(20),
child: Column(children: [
TopicWrap(),
Container(
height: 40,
decoration: BoxDecoration(
borderRadius: BorderRadius.all((Radius.circular(5))),
color: Colors.green),
child: MaterialButton(
textColor: Colors.white,
onPressed: () => {
///新增代碼
print('選擇完畢-${ Provider.of<Topics>(context).chooseTopics}')
},
child: Text('選擇'),
),
),
]),
),
));
}
當(dāng)我們點(diǎn)擊按鈕時會報(bào)錯
Error: Could not find the correct Provider<Topics> above this ChooseTopicPage Widget
This happens because you used a `BuildContext` that does not include the provider
of your choice. There are a few common scenarios:
為什么同為Child
的TopicWrap
不會有這個異常歼跟?
這是因?yàn)?br>
Provider
是根據(jù)InheritedWidget
去實(shí)現(xiàn)的,是根據(jù)父子關(guān)系視圖樹進(jìn)行共享書嫉妒而實(shí)現(xiàn)
TopicWrap
是由一個新的BuildContext去創(chuàng)建的格遭,他已經(jīng)擁有了所有Parent節(jié)點(diǎn)包括Provider哈街,所以它可以獲取到Provider以及它里面的共享數(shù)據(jù),而MaterialButton
是和Provider在一個BuildContext下拒迅,他們是在同一個BuildContext下創(chuàng)建以及初始化,MaterialButton
并非Provider的后代控件
解決: 我們可以使用Provider
的build
屬性他返回一個新的Context
供我們使用骚秦,基于新的context
我們是可以拿到Provider
的
body: ChangeNotifierProvider<Topics>(
create: (_) => _topics,
builder: (context,child){
return Container(
padding: EdgeInsets.all(20),
child: Column(children: [
TopicWrap(),
Container(
height: 40,
decoration: BoxDecoration(
borderRadius: BorderRadius.all((Radius.circular(5))),
color: Colors.green),
child: MaterialButton(
textColor: Colors.white,
onPressed: () => {
print('選擇完畢-${ Provider.of<Topics>(context,listen: false).chooseTopics}')
},
child: Text('選擇'),
),
),
]),
);
},
)
我們也可以使用Consumer去優(yōu)化我們的Provider
處理,讓我們只是刷新數(shù)據(jù)變化的部分不用調(diào)用build
函數(shù)璧微,而且也不需要在頂層去聲明Provider
嵌套復(fù)雜
- 它不需要在在頂層預(yù)先申明一個Provider
- 并且只是更新數(shù)據(jù)變化的局部作箍,而不是重新調(diào)用整個
buld
函數(shù)
使用Consumer包裹你使用到共享數(shù)據(jù)的控件
使用builder
屬性可以拿到新的context
以及共享數(shù)據(jù),然后當(dāng)共享數(shù)據(jù)發(fā)生變化時前硫,你也會進(jìn)行此控件的局部刷新并不會調(diào)用當(dāng)前的build
做到局部刷新胞得,也不用考慮控件的層級
ChangeNotifierProvider<Topics>(
create: (_) => _topics,
child: Container(
padding: EdgeInsets.all(20),
child: Column(children: [
TopicWrap(),
Consumer<Topics>(builder: (context, topics, child) {
return Text('已選擇${topics.chooseTopics}');
}),
Container(
height: 40,
decoration: BoxDecoration(
borderRadius: BorderRadius.all((Radius.circular(5))),
color: Colors.green),
child: MaterialButton(
textColor: Colors.white,
onPressed: () => {
print(
'選擇完畢-${Provider.of<Topics>(context, listen: false).chooseTopics}')
},
child: Text('選擇'),
),
),
]),
),
)
歡迎關(guān)注Mike的簡書
Android 知識整理