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

前言

我們通過一個(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)用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ù)在刷新的時(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)是:

圖片1.png

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

為什么同為ChildTopicWrap不會有這個(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的后代控件

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

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

Android 知識整理

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容