Fiutter- 案例4 (選擇話題Provider)

前言

我們通過一個實(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)用WidgetdidChangeDependencies函數(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);
      });
    });
  }

五秒之后會打印剔应,并且Page1Page2都會刷新助赞,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)是:

圖片1.png

這樣可以實(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:

為什么同為ChildTopicWrap不會有這個異常歼跟?

這是因?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的后代控件

解決: 我們可以使用Providerbuild屬性他返回一個新的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('選擇'),
                ),
              ),
            ]),
          ),
        )
微信圖片_20220605161659.jpg

歡迎關(guān)注Mike的簡書

Android 知識整理

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市屹电,隨后出現(xiàn)的幾起案子阶剑,更是在濱河造成了極大的恐慌跃巡,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件个扰,死亡現(xiàn)場離奇詭異瓷炮,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)递宅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來苍狰,“玉大人办龄,你說我怎么就攤上這事×苷眩” “怎么了俐填?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長翔忽。 經(jīng)常有香客問我英融,道長,這世上最難降的妖魔是什么歇式? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任驶悟,我火速辦了婚禮,結(jié)果婚禮上材失,老公的妹妹穿的比我還像新娘痕鳍。我一直安慰自己,他們只是感情好龙巨,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布笼呆。 她就那樣靜靜地躺著,像睡著了一般旨别。 火紅的嫁衣襯著肌膚如雪诗赌。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天秸弛,我揣著相機(jī)與錄音铭若,去河邊找鬼。 笑死胆屿,一個胖子當(dāng)著我的面吹牛奥喻,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播非迹,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼环鲤,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了憎兽?” 一聲冷哼從身側(cè)響起冷离,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤吵冒,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后西剥,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體痹栖,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年瞭空,在試婚紗的時候發(fā)現(xiàn)自己被綠了揪阿。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡咆畏,死狀恐怖南捂,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情旧找,我是刑警寧澤溺健,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站钮蛛,受9級特大地震影響鞭缭,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜魏颓,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一岭辣、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧琼开,春花似錦易结、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至渣刷,卻和暖如春鹦肿,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背辅柴。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工箩溃, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人碌嘀。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓涣旨,卻偏偏與公主長得像,于是被迫代替她去往敵國和親股冗。 傳聞我的和親對象是個殘疾皇子霹陡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評論 2 355

推薦閱讀更多精彩內(nèi)容