前言
我們通過一個(gè)實(shí)際頁面來使用并且理解一下Flutter Provider的使用,了解下Provider如果進(jìn)行狀態(tài)存儲以及共享
頁面
![]() 微信圖片_20220604131605.jpg
|
![]() 微信圖片_20220604131740.jpg
|
InheritedWidget
InheritedWidget是一個(gè)功能性的組件,可以在組件樹種從上往下的進(jìn)行數(shù)據(jù)共享,定義在InheritedWidget組件中的數(shù)據(jù)可以被其子節(jié)點(diǎn)獲取到,并且當(dāng)InheritedWidget刷新時(shí),所以依賴它的子節(jié)點(diǎn)都會進(jìn)行刷新
創(chuàng)建一個(gè)需要共享的數(shù)據(jù)類
class ShareData{
bool isEnableBiometric;
void setIsEnableBiometric(bool isEnableBiometric){
this.isEnableBiometric = isEnableBiometric;
}
ShareData(this.isEnableBiometric);
}
創(chuàng)建一個(gè)InheritedWidget
里面包含需要被共享的數(shù)據(jù)
class ShareWidget extends InheritedWidget {
ShareData shareData;
static ShareWidget? of(BuildContext context) {
//會將調(diào)用該方法的Widget進(jìn)行注冊,當(dāng)數(shù)據(jù)刷新時(shí)咋會對其進(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) {
///框架是否通知繼承于這個(gè)組件并注冊的子組件
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時(shí)子Widget會進(jìn)行注冊,從而在日后InheritedWidget變化時(shí)可以被刷新,當(dāng)組件樹中的InheritedWidget更新時(shí)會通知所有已經(jīng)注冊的子組件進(jìn)行刷新狀態(tài)
updateShouldNotify
表示已經(jīng)注冊的子Widget是否會在InheritedWidget變化時(shí)刷新,當(dāng)子Widget被通知刷新時(shí)會調(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ù)在刷新的時(shí)候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');
}
}
此時(shí)進(jìn)行數(shù)據(jù)刷新,則Page3不會調(diào)用didChangeDependencies,但是數(shù)據(jù)依舊刷新了,因?yàn)楦腹?jié)點(diǎn)在重新構(gòu)建時(shí),子節(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)建的一個(gè)更好用的全局狀態(tài)管理,數(shù)據(jù)共享,局部刷新框架
我們來使用Provider框架去實(shí)現(xiàn)我們最開偷那兩張圖片的案例
首先我們添加provider依賴
provider: ^4.0.4
簡單分析一下案例
案例中我們需要選擇多個(gè)興趣標(biāo)簽,并且會顯示你已經(jīng)選擇了幾個(gè)標(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.每個(gè)興趣的item需要使用到共享數(shù)據(jù)用以判斷當(dāng)前item是否需要被選中(變色)
2.已選擇數(shù)量所展示的Text
3.底部的提交按鈕需要用到共享數(shù)據(jù),但是只是使用而已
Provider的介紹
它是對InheritedWidget的一個(gè)分封裝以及擴(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是一個(gè)封裝的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í)候也是使用同樣的方式獲取共享數(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ù)變化都個(gè)要刷新整個(gè)Wrap頁面從而達(dá)到對應(yīng)的效果
ChangeNotifyProvider
使用ChangeNotifyProvider,它會在數(shù)據(jù)更新之后通知所有注冊的Widget去進(jìn)行build,不用我們手動的去更新頁面
修改數(shù)據(jù)類
ChangeProvider需要一個(gè)ChangeNotifier類型的共享數(shù)據(jù),并且需要在數(shù)據(jù)被操作需要刷新時(shí)調(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
在我們的案例中底部還有一個(gè)選擇的確認(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)擊按鈕時(shí)會報(bào)錯(cuò)
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不會有這個(gè)異常?
這是因?yàn)?br>
Provider是根據(jù)InheritedWidget去實(shí)現(xiàn)的,是根據(jù)父子關(guān)系視圖樹進(jìn)行共享書嫉妒而實(shí)現(xiàn)
TopicWrap 是由一個(gè)新的BuildContext去創(chuàng)建的,他已經(jīng)擁有了所有Parent節(jié)點(diǎn)包括Provider,所以它可以獲取到Provider以及它里面的共享數(shù)據(jù),而MaterialButton是和Provider在一個(gè)BuildContext下,他們是在同一個(gè)BuildContext下創(chuàng)建以及初始化,MaterialButton并非Provider的后代控件
解決: 我們可以使用Provider的build屬性他返回一個(gè)新的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ù)先申明一個(gè)Provider
- 并且只是更新數(shù)據(jù)變化的局部,而不是重新調(diào)用整個(gè)
buld函數(shù)
使用Consumer包裹你使用到共享數(shù)據(jù)的控件
使用builder屬性可以拿到新的context以及共享數(shù)據(jù),然后當(dāng)共享數(shù)據(jù)發(fā)生變化時(shí),你也會進(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 知識整理

