Flutter系列十:Flutter狀態(tài)管理之Provider的使用和架構(gòu)分析

狀態(tài)管理在Flutter中非常重要,但是它包含的內(nèi)容又非常的廣泛。

本文我們首先了解下什么是狀態(tài)狀態(tài)管理呢?然后我們來了解官方的狀態(tài)管理庫Provider的使用,最后分析下Provider背后的秘密。

Provider

狀態(tài)管理

狀態(tài)

Flutter是聲明式編程,Widget定義的UI都是在build()函數(shù)中實(shí)現(xiàn)的,這個(gè)函數(shù)的功能就是將狀態(tài)轉(zhuǎn)換成UI

UI = f(state)

官方對(duì)狀態(tài)的定義如下:

whatever data you need in order to rebuild your UI at any moment in time

翻譯過來就是:狀態(tài)就是任何時(shí)間任何場(chǎng)景下重構(gòu)UI所需要的數(shù)據(jù)。

這里面至少可以看到兩層含義:

  1. 狀態(tài)就是數(shù)據(jù);
  2. 狀態(tài)的改變驅(qū)動(dòng)了UI的改變。

狀態(tài)的分類

我們可以把狀態(tài)分為局部狀態(tài)全局狀態(tài)

局部狀態(tài)就是Widget內(nèi)部持有的狀態(tài),典型代表就是StatefuleWidget和它對(duì)應(yīng)的State局部狀態(tài)只會(huì)影響單個(gè)Widget的UI呈現(xiàn)。

當(dāng)某個(gè)狀態(tài)需要在多個(gè)Widget使用,或者在整個(gè)APP中使用,那它就是全局狀態(tài)了。全局狀態(tài)的典型代表就是InheritedWidget

我們?cè)?a target="_blank">InheritedWidget的使用和源碼分析這篇文章中已經(jīng)詳細(xì)介紹過了InheritedWidget的相關(guān)內(nèi)容,當(dāng)然我們也提到過它的一些不是太完善的地方。

狀態(tài)管理庫

我們這里所說的狀態(tài)管理庫主要是指對(duì)全局狀態(tài)的一些處理庫,除了InheritedWidget外,還有一些最近非常流行的庫:

它目前是評(píng)分最高的庫,適合大型的項(xiàng)目。但是它有一個(gè)缺點(diǎn)就是理解起來比較困難,編寫代碼方式也很獨(dú)特,需要編寫一些重復(fù)的代碼模板。

它是Flutter官方團(tuán)隊(duì)共同維護(hù)的一個(gè)項(xiàng)目,由于有官方背景,所以不用擔(dān)心后期的維護(hù)升級(jí)問題。

getx是目前上升趨勢(shì)最快的一個(gè)庫,使用非常簡(jiǎn)單,代碼也很簡(jiǎn)介,功能很多。

當(dāng)然還有其他一些庫,譬如mobx,flutter_redux等,當(dāng)然你很大可能也不會(huì)用到。

我們將會(huì)對(duì)Providergetx這兩個(gè)庫的使用和源碼進(jìn)行介紹。

Provider的使用

和介紹InheritedWidget時(shí)使用的案例類似,本文講解Provider的時(shí)候使用是一個(gè)簡(jiǎn)單的計(jì)數(shù)器案例:有一個(gè)number全局狀態(tài),有三個(gè)Widget會(huì)使用到它,點(diǎn)擊FloatingActionButton可以將number的值加1。效果如下:

Demo

當(dāng)然復(fù)雜的多界面邏輯的實(shí)現(xiàn)方法使用的方法是一樣的。譬如實(shí)現(xiàn)下面的功能:


官方的示例

基本使用

使用前得先引入庫:

dependencies:
  provider: ^5.0.0

接下來我們分三步來了解它的使用:

  1. number封裝到ChangeNotifier中,創(chuàng)建需要共享的狀態(tài)
class NumberModel extends ChangeNotifier {
  int _number = 0;

  int get number => _number;

  set number(int value) {
    _number = value;
    notifyListeners();
  }
}

ChangeNotifierFlutter Framework的基礎(chǔ)類,不是Provider庫中的類。ChangeNotifier繼承自Listenable,也就是ChangeNotifier可以通知觀察者值的改變(實(shí)現(xiàn)了觀察者模式)。

NumberModel有一個(gè)_number狀態(tài),然后提供了獲取的方法get和設(shè)置set的方法。

  1. 在應(yīng)用程序的頂層添加ChangeNotifierProvider
void main() {
  runApp(
    ChangeNotifierProvider(
      create: (ctx) => NumberModel(),
      child: MyApp(),
    ),
  );
}

將應(yīng)用的頂層設(shè)置為ChangeNotifierProvider, 然后將MyApp()變?yōu)樗?strong>子Widget。

ChangeNotifierProvidercreate函數(shù)需要返回ChangeNotifier

  1. 其它Widget使用共享的狀態(tài)

有四個(gè)地方需要使用到共享的狀態(tài),三個(gè)顯示文字的Text WidgetFloatingActionButton

  • Provider.of
class NumberWidget1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 獲取NumberModel的number
    int number = Provider.of<NumberModel>(context).number;
    return Container(
      child: Text(
        "點(diǎn)擊次數(shù): $number",
        style: TextStyle(fontSize: 30),
      ),
    );
  }
}

我們將Text Widget封裝成了NumberWidget1, 通過int number = Provider.of<NumberModel>(context).number;獲取到NumberModelnumber值,然后就可以顯示了。

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 1 獲取NumberModel
    NumberModel model = Provider.of<NumberModel>(context);

    return Scaffold(
        appBar: AppBar(
          title: Text("Provider"),
        ),
        body: Center(
          child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [NumberWidget1(), NumberWidget1(), NumberWidget1()]),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            // 2 修改number值
            model.number++;
          },
          child: Icon(Icons.add),
        ));
  }
}

FloatingActionButton也需要通過Provider.of<NumberModel>(context)方法先拿到NumberModel,然后調(diào)用set方法改變number的值。

全部代碼:

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (ctx) => NumberModel(),
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
          primarySwatch: Colors.blue, splashColor: Colors.transparent),
      home: HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    
    NumberModel model = Provider.of<NumberModel>(context);

    return Scaffold(
        appBar: AppBar(
          title: Text("Provider"),
        ),
        body: Center(
          child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [NumberWidget1(), NumberWidget1(), NumberWidget1()]),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            model.number++;
          },
          child: Icon(Icons.add),
        ));
  }
}

class NumberWidget1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    int number = Provider.of<NumberModel>(context).number;
    return Container(
      child: Text(
        "點(diǎn)擊次數(shù): $number",
        style: TextStyle(fontSize: 30),
      ),
    );
  }
}

class NumberModel extends ChangeNotifier {
  int _number = 0;

  int get number => _number;

  set number(int value) {
    _number = value;
    notifyListeners();
  }
}
  • Consumer

問題:Provider.of有一個(gè)問題,就是當(dāng)狀態(tài)值發(fā)生變化后,Provider.of所在的Widget整個(gè)build方法都會(huì)重新構(gòu)建。

上面的例子中,FloatingActionButton會(huì)引起Scaffold的重構(gòu),所以對(duì)性能的影響是最大的。

Consumer<NumberModel>(
    builder: (context, value, child) {
        return FloatingActionButton(
            onPressed: () {
                value.number++;
            },
        child: Icon(Icons.add),
        );
    },
)

我們將FloatingActionButtonConsumer包裹,builder中的value參數(shù)就是我們需要的NumberModel了。

這里我們可以進(jìn)一步優(yōu)化一下,對(duì)child進(jìn)行復(fù)用。

Consumer<NumberModel>(
    builder: (context, value, child) {
        return FloatingActionButton(
            onPressed: () {
                value.number++;
            },
            child: child,
        );
        },
    child: Icon(Icons.add),
));

我們將child傳入Consumer的構(gòu)造函數(shù)就能實(shí)現(xiàn)復(fù)用了。

child復(fù)用的邏輯我們?cè)谇耙黄P(guān)于動(dòng)畫源碼的文章中有解釋,如果需要可以回頭參閱。

差異部分的代碼如下:

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Provider"),
        ),
        body: Center(
          child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [NumberWidget1(), NumberWidget1(), NumberWidget1()]),
        ),
        floatingActionButton: Consumer<NumberModel>(
          builder: (context, value, child) {
            return FloatingActionButton(
              onPressed: () {
                value.number++;
              },
              child: child,
            );
          },
          child: Icon(Icons.add),
        ));
  }
}
  • Consumer

問題:Consumer總歸還是需要重構(gòu)的,其實(shí)我們使用FloatingActionButton的時(shí)候只是用到了NumberModel的設(shè)置方法,根本沒有用到它的_number屬性,所以即使_number改變了,我們也是可以不需要重構(gòu)的。

如果不需要重構(gòu),我們可以使用Selector

Selector<NumberModel, NumberModel>(
    selector: (ctx, numberModel) => numberModel,
    shouldRebuild: (previous, next) => false,
    builder: (context, value, child) {
        return FloatingActionButton(
            onPressed: () {
                value.number++;
            },
        child: child,
        );
    },
    child: Icon(Icons.add),
)

代碼解釋:

  1. Selector的泛型中有兩個(gè)參數(shù)類型,第一個(gè)是原始類型,第二個(gè)是轉(zhuǎn)換后的類型,也就是說Selector多了一個(gè)對(duì)數(shù)據(jù)進(jìn)行轉(zhuǎn)換的功能;
  2. selector是進(jìn)行數(shù)據(jù)類型轉(zhuǎn)換的函數(shù);
  3. shouldRebuild是確實(shí)是否需要重構(gòu),我們明顯是不需要的,所以傳false;
  4. builderConsumer的功能就是類似的了。

差異部分的代碼如下:

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Provider"),
        ),
        body: Center(
          child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [NumberWidget1(), NumberWidget1(), NumberWidget1()]),
        ),
        floatingActionButton: Selector<NumberModel, NumberModel>(
          selector: (ctx, numberModel) => numberModel,
          shouldRebuild: (previous, next) => false,
          builder: (context, value, child) {
            return FloatingActionButton(
              onPressed: () {
                value.number++;
              },
              child: child,
            );
          },
          child: Icon(Icons.add),
        ));
  }
}

多個(gè)狀態(tài)的使用

有時(shí)候某個(gè)Widget可能需要使用多個(gè)狀態(tài),我們接下來就介紹這種情況的使用方法。

  1. 創(chuàng)建多個(gè)需要共享的狀態(tài)
class RandomNumberModel extends ChangeNotifier {
  int _randomNumber = Random().nextInt(100);

  int get randomNumber => _randomNumber;

  void resetRandomNumber() {
    _randomNumber = Random().nextInt(100);
    notifyListeners();
  }
}

我們?cè)賱?chuàng)建一個(gè)RandomNumberModel,里面有一個(gè)隨機(jī)的數(shù)值_randomNumber, 并且設(shè)置獲取方法get和設(shè)置方法resetRandomNumber

  1. 將應(yīng)用程序的頂層改為MultiProvider
void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider<NumberModel>(
          create: (ctx) => NumberModel(),
        ),
        ChangeNotifierProvider<RandomNumberModel>(
          create: (ctx) => RandomNumberModel(),
        ),
      ],
      child: MyApp(),
    ),
  );
}

MultiProviderproviders放置的是共享的多個(gè)Provider

  1. 其它Widget使用共享的狀態(tài)
  • Provider.of
class NumberWidget1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 讀取
    int number = Provider.of<NumberModel>(context).number;
    // 讀取
    int randomNumber = Provider.of<RandomNumberModel>(context).randomNumber;
    return Container(
      // 使用
      child: Text(
        "點(diǎn)擊次數(shù): $number 隨機(jī)數(shù): $randomNumber",
        style: TextStyle(fontSize: 30),
      ),
    );
  }
}

我們可以通過Provider.of分別取到NumberModelRandomNumberModel,然后讀取到相應(yīng)的值。

  • Consumer2
class NumberWidget2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Consumer2<NumberModel, RandomNumberModel>(
        builder: (context, value, value2, child) {
          return Text("點(diǎn)擊次數(shù): ${value.number}  隨機(jī)數(shù): ${value2.randomNumber}",
              style: TextStyle(fontSize: 30));
        },
      ),
    );
  }
}

Consumer2中兩個(gè)泛型代表使用的哪兩個(gè)數(shù)據(jù),build方法中的value就是NumberModel,value2就是RandomNumberModel,然后讀取到相應(yīng)的值。

  • Selector2
class NumberWidget3 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Selector2<NumberModel, RandomNumberModel, Tuple2<int, int>>(
        selector: (ctx, value1, value2) => Tuple2(value1.number, value2.randomNumber),
        builder: (context, value, child) {
          return Text("點(diǎn)擊次數(shù): ${value.item1}  隨機(jī)數(shù): ${value.item2}",
              style: TextStyle(fontSize: 30));
        },
        shouldRebuild: (previous, next) => previous != next,
      )
    );
  }
}
  1. Selector2有三個(gè)泛型參數(shù):NumberModelRandomNumberModel代表使用的兩個(gè)數(shù)據(jù)類型,第三個(gè)參數(shù)表示由前兩個(gè)數(shù)據(jù)轉(zhuǎn)換成的新的數(shù)據(jù)類型,我們需要使用兩個(gè)int值。

使用Tuple2需要引入三方庫 tuple: ^2.0.0。使用它的優(yōu)點(diǎn)是它內(nèi)置了==比較操作符,不需要我們?nèi)プ约罕容^元素是否相等了。

  1. selector的三個(gè)參數(shù)為:BuildContextNumberModelRandomNumberModel, 返回值就是轉(zhuǎn)換后的數(shù)據(jù)。

builder方法中就可以直接使用value.item1value.item2了。

  1. shouldRebuild方法的previousnext的類型是Tuple2<int, int>,可以直接比較。如果相同就不重構(gòu)了。

多個(gè)狀態(tài)使用的補(bǔ)充

Consumer2還有幾個(gè)好兄弟:,Consumer3Consumer4Consumer5Consumer6

Selector2也有幾個(gè)好兄弟:,Selector3Selector4Selector5Selector6

通過名字可以知道,他們分別可以組合對(duì)應(yīng)的多個(gè)數(shù)據(jù)。

全部代碼:

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider<NumberModel>(
          create: (ctx) => NumberModel(),
        ),
        ChangeNotifierProvider<RandomNumberModel>(
          create: (ctx) => RandomNumberModel(),
        ),
      ],
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
          primarySwatch: Colors.blue, splashColor: Colors.transparent),
      home: HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Provider"),
      ),
      body: Center(
        child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [NumberWidget1(), NumberWidget2(), NumberWidget3()]),
      ),
      floatingActionButton: Consumer2<NumberModel, RandomNumberModel>(
        child: Icon(Icons.add),
        builder: (context, value, value2, child) {
          return FloatingActionButton(
            onPressed: () {
              value.number++;
              value2.resetRandomNumber();
            },
            child: child,
          );
        },
      ),
    );
  }
}

class NumberWidget1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    int number = Provider.of<NumberModel>(context).number;
    int randomNumber = Provider.of<RandomNumberModel>(context).randomNumber;
    return Container(
      child: Text(
        "點(diǎn)擊次數(shù): $number 隨機(jī)數(shù): $randomNumber",
        style: TextStyle(fontSize: 30),
      ),
    );
  }
}

class NumberWidget2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Consumer2<NumberModel, RandomNumberModel>(
        builder: (context, value, value2, child) {
          return Text("點(diǎn)擊次數(shù): ${value.number}  隨機(jī)數(shù): ${value2.randomNumber}",
              style: TextStyle(fontSize: 30));
        },
      ),
    );
  }
}

class NumberWidget3 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Selector2<NumberModel, RandomNumberModel, Tuple2<int, int>>(
        selector: (ctx, value1, value2) => Tuple2(value1.number, value2.randomNumber),
        builder: (context, value, child) {
          return Text("點(diǎn)擊次數(shù): ${value.item1}  隨機(jī)數(shù): ${value.item2}",
              style: TextStyle(fontSize: 30));
        },
        shouldRebuild: (previous, next) => previous != next,
      )
    );
  }
}

class NumberModel extends ChangeNotifier {
  int _number = 0;

  int get number => _number;

  set number(int value) {
    _number = value;
    notifyListeners();
  }
}

class RandomNumberModel extends ChangeNotifier {
  int _randomNumber = Random().nextInt(100);

  int get randomNumber => _randomNumber;

  void resetRandomNumber() {
    _randomNumber = Random().nextInt(100);
    notifyListeners();
  }
}

Provider源碼解析

  • Provider的基本架構(gòu)如下:
Provider架構(gòu)
  1. 所有的Provider都繼承自InheritedProvider
  2. InheritedProvider持有一個(gè)_CreateInheritedProvider對(duì)象_delegate, _delegate持有_ValueInheritedProviderState對(duì)象,_ValueInheritedProviderState對(duì)象通過createState()方法調(diào)用了InheritedProvidercreate()方法生成了_value_value也就是開發(fā)者提供的可監(jiān)測(cè)對(duì)象ChangeNotifier;

create()只有在需要使用_value時(shí)候才會(huì)調(diào)用,并不是InheritedProvider插入Widget Tree時(shí)候就調(diào)用,屬于懶加載的實(shí)現(xiàn)。

  1. InheritedProvider有一個(gè)InheritedWidget子Widget _InheritedProviderScope。_InheritedProviderScope持有上面提到的_value的值;

也就是說Provider依賴于InheritedWidget,找到對(duì)應(yīng)的InheritedWidget就能獲取對(duì)應(yīng)的_value的值。

  1. Widget重構(gòu)的時(shí)候如果調(diào)用Provider.of方法,會(huì)找到_value的值并且監(jiān)聽它的變化。
  • Provider的局部刷新邏輯如下:
Provider的局部刷新
  1. _value值發(fā)生變化,會(huì)通知監(jiān)聽者刷新。其中會(huì)調(diào)用_InheritedProviderScope的markNeedsNotifyDependents方法,調(diào)用依賴WidgetdidChangeDependencies, 這兩個(gè)方法都會(huì)調(diào)用markNeedsBuild(),進(jìn)行重構(gòu);
  2. Widget重構(gòu)的時(shí)候會(huì)調(diào)用Provider.of方法,更新對(duì)_value的監(jiān)聽,為下次重構(gòu)做準(zhǔn)備。
  • ConsumerSelector的優(yōu)化邏輯:
Consumer

ConsumerSelector只是封裝了一層SingleChildStatefulWidget,重構(gòu)的范圍限定在ConsumerSelector內(nèi)部,內(nèi)部調(diào)用的還是Provider.of方法。

  • MultiProvider的邏輯:
多個(gè)Provider

MultiProvider就是嵌套了多個(gè)Provider,其他和單個(gè)Provider沒有什么差別。

總結(jié)

其實(shí)Provider庫還提供了其他的幾個(gè)ProviderListenableProvider,ValueListenableProvider,StreamProviderFutureProvider,它們都是我們開發(fā)中的可選項(xiàng)。

至此,我們將Provider庫的使用方式和底層的邏輯解釋完了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過簡(jiǎn)信或評(píng)論聯(lián)系作者。

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