Flutter Provider狀態管理---四種消費者使用分析

Jimi 發表於 2021-10-13
Flutter

文章系列

Flutter Provider狀態管理---介紹、類圖分析、基本使用

Flutter Provider狀態管理---八種提供者使用分析

Flutter Provider狀態管理---四種消費者使用分析

Flutter Provider狀態管理---MVVM架構實戰

視訊系列

Flutter Provider狀態管理---介紹、類圖分析、基本使用

Flutter Provider狀態管理---八種提供者使用分析

Flutter Provider狀態管理---四種消費者使用分析

Flutter Provider狀態管理---MVVM架構實戰

原始碼倉庫地址

github倉庫地址

前言

在上一篇文章中我們對Provider的8種提供者進行了詳細的描述以及用對應的案例說明他們的區別,那麼這一節我們來聊一聊Provider的消費者,如果去優化你的專案結構以及它們的使用區別。

Provider.of

Provider.of<T>(context)Provider為我們提供的靜態方法,當我們使用該方法去獲取值的時候會返回查詢到的最近的T型別的provider給我們,而且也不會遍歷整個元件樹,下面我們看下程式碼:

第一步:定義模型

我們定義了一個CountNotifier1的模型,後面所有的示例程式碼將圍繞該模型來演示

import 'package:flutter/material.dart';

class CountNotifier1 with ChangeNotifier {

  int count = 0;

  void increment() {
    count++;
    notifyListeners();
  }
}

第二步:應用程式入口設定

return ChangeNotifierProvider(
  create: (_) => CountNotifier1(),
  child: MaterialApp(
    debugShowCheckedModeBanner: false,
    home: ConsumerExample(),
  ),
);

第三步:使用Provider.of

這裡讀取值和點選按鈕+1時都是通過Provider.of<T>()來獲取及使用。

import 'package:flutter/material.dart';
import 'package:flutter_provider_example/consumer_example/count_notifier1.dart';
import 'package:provider/provider.dart';

class ConsumerExample extends StatelessWidget {

  @override
  Widget build(BuildContext context) {

    return Scaffold(
      appBar: AppBar(
        title: Text("ConsumerExample"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(Provider.of<CountNotifier1>(context).count.toString(),
              style: TextStyle(
                  color: Colors.red,
                  fontSize: 50
              ),
            ),
            Padding(
              padding: EdgeInsets.only(
                top: 20
              ),
              child: ElevatedButton(
                onPressed: (){
                  Provider.of<CountNotifier1>(context).increment();
                },
                child: Text("點選加1"),
              ),
            )
          ],
        ),
      ),
    );
  }
}

錯誤日誌

當我們執行程式碼的時候會提示一個報錯,它提示說試圖從Widget樹外部監聽提供者公開的值,如果要修復可以把listen改成false,這個問題其實是在Provider 4.0.2後會出現的,最主要的是它的預設行為就是ture,錯誤日誌如下:

======== Exception caught by gesture ===============================================================
The following assertion was thrown while handling a gesture:
Tried to listen to a value exposed with provider, from outside of the widget tree.

This is likely caused by an event handler (like a button's onPressed) that called
Provider.of without passing `listen: false`.

To fix, write:
Provider.of<CountNotifier1>(context, listen: false);

It is unsupported because may pointlessly rebuild the widget associated to the
event handler, when the widget tree doesn't care about the value.

The context used was: ConsumerExample(dependencies: [_InheritedProviderScope<CountNotifier1?>])
'package:provider/src/provider.dart':
Failed assertion: line 276 pos 7: 'context.owner!.debugBuilding ||
          listen == false ||
          debugIsInInheritedProviderUpdate'

When the exception was thrown, this was the stack: 
........
====================================================================================================

設定listen為false

Provider.of<CountNotifier1>(context, listen: false).increment();

執行結果

Flutter Provider狀態管理---四種消費者使用分析

Consumer

Consumber只是在Widget中呼叫了Prvoider.of,並將其構造實現委託給了構造器,比如我們常見的Builder,如果你的Widget依賴多個模型,那麼它還提供了Consumer23456方便呼叫,我們接下來對上面的案例採用Consumer來修改

用Consumer包裹元件

裡面有個builder構造器,當我們把body改成下面重新執行後可以發現和使用Provider.of的結果一樣,但是這裡不需要在像使用Provider.of那樣每次使用都要寫一大串的重複性程式碼。

裡面有三個屬性:

  • context: 當前的上下文
  • Provider.of<T>(context): 模型物件
  • child: 子元件(不需要重新整理的部分)
body: Consumer(
  builder: (_, CountNotifier1 countNotifier1, child) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text(countNotifier1.count.toString(),
               style: TextStyle(
                 color: Colors.red,
                 fontSize: 50
               ),
              ),
          Padding(
            padding: EdgeInsets.only(
              top: 20
            ),
            child: ElevatedButton(
              onPressed: (){
                countNotifier1.increment();
              },
              child: Text("點選加1"),
            ),
          )
        ],
      ),
    );
  },
),

優化Consumer

優化方式一:儘可能調整Consumer的位置

我們在上面的程式碼中發現Center以及Column元件也被Consumer包裹了進來,但是這兩個元件是不需要更新狀態的,而我們每次構建的Widget的時候,會重建整個body,所以我們優化一下程式碼結構,看起來就像下面這樣:

body: Center(
  child: Consumer(
    builder: (_, CountNotifier1 countNotifier1, child) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              countNotifier1.count.toString(),
              style: TextStyle(color: Colors.red, fontSize: 50),
            ),
            Padding(
              padding: EdgeInsets.only(top: 20),
              child: ElevatedButton(
                onPressed: () {
                  countNotifier1.increment();
                },
                child: Text("點選加1"),
              ),
            ),
                   Container(
              child: Column(
                children: [
                  Text("更多元件1"),
                  Text("更多元件2"),
                  Text("更多元件3"),
                  Text("更多元件4"),
                  Text("更多元件5"),
                  Text("更多元件6"),
                ],
              ),
            )
          ],
        ),
      );
    },
  )
)

優化方式二:不需要重新整理但被Consumer包裹的元件用child

比如上面我們有更多元件1-6甚至數百個元件無需重新整理狀態,但由於你用Consumer包裹會導致全部重新整理,那麼明顯會導致效能的下降,你可能會想到單獨用多個Consumer包裹需要重新整理的元件就解決了,但這不就是本末倒置了嗎,本身Provider是解決程式碼的健壯重複的程式碼,所以這個時候我們可以採用Consumer為我們提供的child引數,如下:

body: Center(
  child: Consumer(
    builder: (_, CountNotifier1 countNotifier1, child) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              countNotifier1.count.toString(),
              style: TextStyle(color: Colors.red, fontSize: 50),
            ),
            Padding(
              padding: EdgeInsets.only(top: 20),
              child: ElevatedButton(
                onPressed: () {
                  countNotifier1.increment();
                },
                child: Text("點選加1"),
              ),
            ),
            child!
          ],
        ),
      );
    },
    child: Container(
      child: Column(
        children: [
          Text("更多元件1"),
          Text("更多元件2"),
          Text("更多元件3"),
          Text("更多元件4"),
          Text("更多元件5"),
          Text("更多元件6"),
        ],
      ),
    ),
  )
),

Selector

Selector類和Consumer類似,只是對build呼叫Widget方法時提供更精細的控制,簡單點來說,Selector也是一個消費者,它允許你可以從模型中準備定義哪些屬性。

我們來舉個例子:

比如,使用者模型中有50個屬性,但是我只需要更新年齡,這樣我希望不需要重建使用者名稱電話號碼等元件,那麼Selector就是用於解決這個問題,我們看一下示例:

第一步:定義模型

import 'package:flutter/material.dart';

class UserModel6 with ChangeNotifier {

  String name = "Jimi";
  int age = 18;
  String phone = "18888888888";


  void increaseAge() {
    age++;
    notifyListeners();
  }
}

第二步:應用程式入口設定

return ChangeNotifierProvider(
  create: (_) => UserModel6(),
  child: MaterialApp(
    debugShowCheckedModeBanner: false,
    home: SelectorExample(),
  ),
);

第三步:使用Selector更精細的控制

import 'package:flutter/material.dart';
import 'package:flutter_provider_example/selector_example/user_model6.dart';
import 'package:provider/provider.dart';

class SelectorExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("SelectorExample"),
      ),
      body: Center(
        child: Selector<UserModel6, int>(
          selector: (_, userModel6) => userModel6.age,
          builder: (_, age, child) {
            return Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(age.toString(),
                    style: TextStyle(
                        color: Colors.red,
                        fontSize: 30
                    )
                ),
                child!
              ],
            );
          },
          child: Padding(
            padding: EdgeInsets.all(20),
            child: ElevatedButton(
              onPressed: (){
                Provider.of<UserModel6>(context, listen: false).increaseAge();
              },
              child: Text("改變年齡"),
            ),
          ),
        ),
      ),
    );
  }
}

執行結果

Flutter Provider狀態管理---四種消費者使用分析

InheritedContext

InheritedContextProvider內建擴充套件了BuildContext,它不儲存了元件在樹中自己位置的引用,我們在上面的案例中見到Provider.of<CountNotifier1>(context,listen: false),其實這個of方法就是使用Flutter查詢樹並找到Provider子型別為CountNotifier1而已。

三大方式:

  • BuildContext.read: BuildContext.read<CountNotifier1>()可以替換掉Provider.of<CountNotifier1>(context,listen: false),它會找到CountNotifier1並返回它。
  • BuildContext.watch: BuildContext.watch<CountNotifier1>()可以替換掉Provider.of<CountNotifier1>(context,listen: false),看起來和read沒有什麼不同,但是使用watch你就不需要在使用Consumer
  • BuildContext.select: BuildContext.select<CountNotifier1>()可以替換掉Provider.of<CountNotifier1>(context,listen: false),看起來和watch也沒有什麼不同,但是使用select你就不需要在使用Selector

BuildContext.read

下面兩種使用方式結果是一樣的

使用Provider.of<T>()

import 'package:flutter/material.dart';
import 'package:flutter_provider_example/Inherited_context_example/count_notifier2.dart';
import 'package:provider/provider.dart';


class InheritedContextExample extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("InheritedContextExample"),
      ),

      /// Provider.of 獲取值
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(Provider.of<CountNotifier2>(context).count.toString(),
              style: TextStyle(
                  color: Colors.red,
                  fontSize: 50
              ),
            ),
            Padding(
              padding: EdgeInsets.only(top: 20),
              child: ElevatedButton(
                onPressed: () => Provider.of<CountNotifier2>(context, listen: false).increment(),
                child: Text("點選加1"),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

使用BuildContext.read

import 'package:flutter/material.dart';
import 'package:flutter_provider_example/Inherited_context_example/count_notifier2.dart';
import 'package:provider/provider.dart';


class InheritedContextExample extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("InheritedContextExample"),
      ),
      /// read 獲取值
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(context.read<CountNotifier2>().count.toString(),
              style: TextStyle(
                  color: Colors.red,
                  fontSize: 50
              ),
            ),
            Padding(
              padding: EdgeInsets.only(top: 20),
              child: ElevatedButton(
                onPressed: () => Provider.of<CountNotifier2>(context, listen: false).increment(),
                child: Text("點選加1"),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

BuildContext.watch

使用Consumer

import 'package:flutter/material.dart';
import 'package:flutter_provider_example/Inherited_context_example/count_notifier2.dart';
import 'package:provider/provider.dart';

class InheritedContextExample extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("InheritedContextExample"),
      ),
      /// Consumer 獲取值
      body: Center(
        child: Consumer<CountNotifier2>(
          builder: (_, countNotifier2, child) {
            return Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(countNotifier2.count.toString(),
                  style: TextStyle(
                      color: Colors.red,
                      fontSize: 50
                  ),
                ),
                Padding(
                  padding: EdgeInsets.only(top: 20),
                  child: ElevatedButton(
                    onPressed: () => countNotifier2.increment(),
                    child: Text("點選加1"),
                  ),
                ),
              ],
            );
          },
        ),
      ),
    );
  }
}

使用BuildContext.watch

import 'package:flutter/material.dart';
import 'package:flutter_provider_example/Inherited_context_example/count_notifier2.dart';
import 'package:provider/provider.dart';


class InheritedContextExample extends StatelessWidget {

  @override
  Widget build(BuildContext context) {

    /// 重要
    final countNotifier2 = context.watch<CountNotifier2>();

    return Scaffold(
      appBar: AppBar(
        title: Text("InheritedContextExample"),
      ),
      /// watch 
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(countNotifier2.count.toString(),
              style: TextStyle(
                  color: Colors.red,
                  fontSize: 50
              ),
            ),
            Padding(
              padding: EdgeInsets.only(top: 20),
              child: ElevatedButton(
                onPressed: () => countNotifier2.increment(),
                child: Text("點選加1"),
              ),
            ),
          ],
        ),
      ),

    );
  }
}

BuildContext.select

使用Selector

import 'package:flutter/material.dart';
import 'package:flutter_provider_example/Inherited_context_example/count_notifier2.dart';
import 'package:provider/provider.dart';

class InheritedContextExample extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("InheritedContextExample"),
      ),
      /// Selector
      body: Center(
        child: Selector<CountNotifier2, int>(
          selector: (_, countNotifier2) => countNotifier2.count,
          builder: (_, count, child) {
            return Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(count.toString(),
                  style: TextStyle(
                      color: Colors.red,
                      fontSize: 50
                  ),
                ),
                child!
              ],
            );
          },
          child: Padding(
            padding: EdgeInsets.only(top: 20),
            child: ElevatedButton(
              onPressed: () => Provider.of<CountNotifier2>(context, listen: false).increment(),
              child: Text("點選加1"),
            ),
          ),
        ),
      ),

    );
  }
}

使用BuildContext.select

import 'package:flutter/material.dart';
import 'package:flutter_provider_example/Inherited_context_example/count_notifier2.dart';
import 'package:provider/provider.dart';


class InheritedContextExample extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    
    /// 重要
    final count = context.select((CountNotifier2 countNotifier2) => countNotifier2.count);

    return Scaffold(
      appBar: AppBar(
        title: Text("InheritedContextExample"),
      ),
      /// select
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(count.toString(),
              style: TextStyle(
                  color: Colors.red,
                  fontSize: 50
              ),
            ),
            Padding(
              padding: EdgeInsets.only(top: 20),
              child: ElevatedButton(
                onPressed: () => Provider.of<CountNotifier2>(context, listen: false).increment(),
                child: Text("點選加1"),
              ),
            )
          ],
        ),
      ),
    );
  }
}

總結

Flutter為我們提供了多種讀取值的方式,上面我們對消費者四大類的一個使用和分析對比,大家可根據自己的實際應用場景去使用對應的方式。