Flutter狀態管理

Flutter狀態管理

狀態管理是聲明式編程很是重要的一個概念,咱們在前面介紹過Flutter是聲明式編程的,也區分聲明式編程和命令式編程的區別。vue

這裏,咱們就來系統的學習一下Flutter聲明式編程中很是重要的狀態管理git

一. 爲何須要狀態管理?

1.1. 認識狀態管理

不少從命令式編程框架(Android或iOS原生開發者)轉成聲明式編程(Flutter、Vue、React等)剛開始並不適應,由於須要一個新的角度來考慮APP的開發模式。github

Flutter做爲一個現代的框架,是聲明式編程的:web

Flutter構建應用過程
Flutter構建應用過程

在編寫一個應用的過程當中,咱們有大量的State須要來進行管理,而正是對這些State的改變,來更新界面的刷新:算法

狀態管理流程
狀態管理流程

1.2. 不一樣狀態管理分類

1.2.1. 短時狀態Ephemeral state

某些狀態只須要在本身的Widget中使用便可編程

  • 好比咱們以前作的簡單計數器counter
  • 好比一個PageView組件記錄當前的頁面
  • 好比一個動畫記錄當前的進度
  • 好比一個BottomNavigationBar中當前被選中的tab

這種狀態咱們只須要使用StatefulWidget對應的State類本身管理便可,Widget樹中的其它部分並不須要訪問這個狀態。redux

這種方式在以前的學習中,咱們已經應用過很是屢次了。數據結構

1.2.2. 應用狀態App state

開發中也有很是多的狀態須要在多個部分進行共享app

  • 好比用戶一個個性化選項
  • 好比用戶的登陸狀態信息
  • 好比一個電商應用的購物車
  • 好比一個新聞應用的已讀消息或者未讀消息

這種狀態咱們若是在Widget之間傳遞來、傳遞去,那麼是無窮盡的,而且代碼的耦合度會變得很是高,牽一髮而動全身,不管是代碼編寫質量、後期維護、可擴展性都很是差。框架

這個時候咱們能夠選擇全局狀態管理的方式,來對狀態進行統一的管理和應用。

1.2.3. 如何選擇不一樣的管理方式

開發中,沒有明確的規則去區分哪些狀態是短時狀態,哪些狀態是應用狀態。

  • 某些短時狀態可能在以後的開發維護中須要升級爲應用狀態。

可是咱們能夠簡單遵照下面這幅流程圖的規則:

狀態管理選擇
狀態管理選擇

針對React使用setState仍是Redux中的Store來管理狀態哪一個更好的問題,Redux的issue上,Redux的做者Dan Abramov,它這樣回答的:

The rule of thumb is: Do whatever is less awkward

經驗原則就是:選擇可以減小麻煩的方式。

選擇可以減小麻煩的方式
選擇可以減小麻煩的方式

二. 共享狀態管理

2.1. InheritedWidget

InheritedWidget和React中的context功能相似,能夠實現跨組件數據的傳遞。

定義一個共享數據的InheritedWidget,須要繼承自InheritedWidget

  • 這裏定義了一個of方法,該方法經過context開始去查找祖先的HYDataWidget(能夠查看源碼查找過程)
  • updateShouldNotify方法是對比新舊HYDataWidget,是否須要對更新相關依賴的Widget
class HYDataWidget extends InheritedWidget {
 final int counter;   HYDataWidget({this.counter, Widget child}): super(child: child);   static HYDataWidget of(BuildContext context) {  return context.dependOnInheritedWidgetOfExactType();  }   @override  bool updateShouldNotify(HYDataWidget oldWidget) {  return this.counter != oldWidget.counter;  } } 複製代碼

建立HYDataWidget,而且傳入數據(這裏點擊按鈕會修改數據,而且從新build)

class HYHomePage extends StatefulWidget {
 @override  _HYHomePageState createState() => _HYHomePageState(); }  class _HYHomePageState extends State<HYHomePage> {  int data = 100;   @override  Widget build(BuildContext context) {  return Scaffold(  appBar: AppBar(  title: Text("InheritedWidget"),  ),  body: HYDataWidget(  counter: data,  child: Center(  child: Column(  mainAxisAlignment: MainAxisAlignment.center,  children: <Widget>[  HYShowData()  ],  ),  ),  ),  floatingActionButton: FloatingActionButton(  child: Icon(Icons.add),  onPressed: () {  setState(() {  data++;  });  },  ),  );  } } 複製代碼

在某個Widget中使用共享的數據,而且監聽

2.2. Provider

Provider是目前官方推薦的全局狀態管理工具,由社區做者Remi Rousselet 和 Flutter Team共同編寫。

使用以前,咱們須要先引入對它的依賴,截止這篇文章,Provider的最新版本爲4.0.4

dependencies:
 provider: ^4.0.4 複製代碼

2.2.1. Provider的基本使用

在使用Provider的時候,咱們主要關心三個概念:

  • ChangeNotifier:真正數據(狀態)存放的地方
  • ChangeNotifierProvider:Widget樹中提供數據(狀態)的地方,會在其中建立對應的ChangeNotifier
  • Consumer:Widget樹中須要使用數據(狀態)的地方

咱們先來完成一個簡單的案例,將官方計數器案例使用Provider來實現:

第一步:建立本身的ChangeNotifier

咱們須要一個ChangeNotifier來保存咱們的狀態,因此建立它

  • 這裏咱們可使用繼承自ChangeNotifier,也可使用混入,這取決於機率是否須要繼承自其它的類
  • 咱們使用一個私有的_counter,而且提供了getter和setter
  • 在setter中咱們監聽到_counter的改變,就調用notifyListeners方法,通知全部的Consumer進行更新
class CounterProvider extends ChangeNotifier {
 int _counter = 100;  int get counter {  return _counter;  }  set counter(int value) {  _counter = value;  notifyListeners();  } } 複製代碼

第二步:在Widget Tree中插入ChangeNotifierProvider

咱們須要在Widget Tree中插入ChangeNotifierProvider,以便Consumer能夠獲取到數據:

  • 將ChangeNotifierProvider放到了頂層,這樣方便在整個應用的任何地方可使用CounterProvider
void main() {
 runApp(ChangeNotifierProvider(  create: (context) => CounterProvider(),  child: MyApp(),  )); } 複製代碼

第三步:在首頁中使用Consumer引入和修改狀態

  • 引入位置一:在body中使用Consumer,Consumer須要傳入一個builder回調函數,當數據發生變化時,就會通知依賴數據的Consumer從新調用builder方法來構建;
  • 引入位置二:在floatingActionButton中使用Consumer,當點擊按鈕時,修改CounterNotifier中的counter數據;
class HYHomePage extends StatelessWidget {
 @override  Widget build(BuildContext context) {  return Scaffold(  appBar: AppBar(  title: Text("列表測試"),  ),  body: Center(  child: Consumer<CounterProvider>(  builder: (ctx, counterPro, child) {  return Text("當前計數:${counterPro.counter}", style: TextStyle(fontSize: 20, color: Colors.red),);  }  ),  ),  floatingActionButton: Consumer<CounterProvider>(  builder: (ctx, counterPro, child) {  return FloatingActionButton(  child: child,  onPressed: () {  counterPro.counter += 1;  },  );  },  child: Icon(Icons.add),  ),  );  } } 複製代碼

Consumer的builder方法解析:

  • 參數一:context,每一個build方法都會有上下文,目的是知道當前樹的位置
  • 參數二:ChangeNotifier對應的實例,也是咱們在builder函數中主要使用的對象
  • 參數三:child,目的是進行優化,若是builder下面有一顆龐大的子樹,當模型發生改變的時候,咱們並不但願從新build這顆子樹,那麼就能夠將這顆子樹放到Consumer的child中,在這裏直接引入便可(注意我案例中的Icon所放的位置)
案例效果
案例效果

步驟四:建立一個新的頁面,在新的頁面中修改數據

class SecondPage extends StatelessWidget {
 @override  Widget build(BuildContext context) {  return Scaffold(  appBar: AppBar(  title: Text("第二個頁面"),  ),  floatingActionButton: Consumer<CounterProvider>(  builder: (ctx, counterPro, child) {  return FloatingActionButton(  child: child,  onPressed: () {  counterPro.counter += 1;  },  );  },  child: Icon(Icons.add),  ),  );  } } 複製代碼
第二個頁面修改數據
第二個頁面修改數據

2.2.2. Provider.of的弊端

事實上,由於Provider是基於InheritedWidget,因此咱們在使用ChangeNotifier中的數據時,咱們能夠經過Provider.of的方式來使用,好比下面的代碼:

Text("當前計數:${Provider.of<CounterProvider>(context).counter}",
 style: TextStyle(fontSize: 30, color: Colors.purple), ), 複製代碼

咱們會發現很明顯上面的代碼會更加簡潔,那麼開發中是否要選擇上面這種方式了?

  • 答案是否認的,更多時候咱們仍是要選擇Consumer的方式。

爲何呢?由於Consumer在刷新整個Widget樹時,會盡量少的rebuild Widget。

方式一:Provider.of的方式完整的代碼:

  • 當咱們點擊了floatingActionButton時,HYHomePage的build方法會被從新調用。
  • 這意味着整個HYHomePage的Widget都須要從新build
class HYHomePage extends StatelessWidget {
 @override  Widget build(BuildContext context) {  print("調用了HYHomePage的build方法");  return Scaffold(  appBar: AppBar(  title: Text("Provider"),  ),  body: Center(  child: Column(  mainAxisAlignment: MainAxisAlignment.center,  children: <Widget>[  Text("當前計數:${Provider.of<CounterProvider>(context).counter}",  style: TextStyle(fontSize: 30, color: Colors.purple),  )  ],  ),  ),  floatingActionButton: Consumer<CounterProvider>(  builder: (ctx, counterPro, child) {  return FloatingActionButton(  child: child,  onPressed: () {  counterPro.counter += 1;  },  );  },  child: Icon(Icons.add),  ),  );  } } 複製代碼

方式二:將Text中的內容採用Consumer的方式修改以下:

  • 你會發現HYHomePage的build方法不會被從新調用;
  • 設置若是咱們有對應的child widget,能夠採用上面案例中的方式來組織,性能更高;
Consumer<CounterProvider>(builder: (ctx, counterPro, child) {
 print("調用Consumer的builder");  return Text(  "當前計數:${counterPro.counter}",  style: TextStyle(fontSize: 30, color: Colors.red),  ); }), 複製代碼

2.2.3. Selector的選擇

Consumer是不是最好的選擇呢?並非,它也會存在弊端

  • 好比當點擊了floatingActionButton時,咱們在代碼的兩處分別打印它們的builder是否會從新調用;
  • 咱們會發現只要點擊了floatingActionButton,兩個位置都會被從新builder;
  • 可是floatingActionButton的位置有從新build的必要嗎?沒有,由於它是否在操做數據,並無展現;
  • 如何能夠作到讓它不要從新build了?使用Selector來代替Consumer
Select的弊端
Select的弊端

咱們先直接實現代碼,在解釋其中的含義:

floatingActionButton: Selector<CounterProvider, CounterProvider>(
 selector: (ctx, provider) => provider,  shouldRebuild: (pre, next) => false,  builder: (ctx, counterPro, child) {  print("floatingActionButton展現的位置builder被調用");  return FloatingActionButton(  child: child,  onPressed: () {  counterPro.counter += 1;  },  );  },  child: Icon(Icons.add), ), 複製代碼

Selector和Consumer對比,不一樣之處主要是三個關鍵點:

  • 關鍵點1:泛型參數是兩個
    • 泛型參數一:咱們此次要使用的Provider
    • 泛型參數二:轉換以後的數據類型,好比我這裏轉換以後依然是使用CounterProvider,那麼他們兩個就是同樣的類型
  • 關鍵點2:selector回調函數
    • 轉換的回調函數,你但願如何進行轉換
    • S Function(BuildContext, A) selector
    • 我這裏沒有進行轉換,因此直接將A實例返回便可
  • 關鍵點3:是否但願從新rebuild
    • 這裏也是一個回調函數,咱們能夠拿到轉換先後的兩個實例;
    • bool Function(T previous, T next);
    • 由於這裏我不但願它從新rebuild,不管數據如何變化,因此這裏我直接return false;
Selector的使用
Selector的使用

這個時候,咱們從新測試點擊floatingActionButton,floatingActionButton中的代碼並不會進行rebuild操做。

因此在某些狀況下,咱們可使用Selector來代替Consumer,性能會更高。

2.2.4. MultiProvider

在開發中,咱們須要共享的數據確定不止一個,而且數據之間咱們須要組織到一塊兒,因此一個Provider必然是不夠的。

咱們在增長一個新的ChangeNotifier

import 'package:flutter/material.dart';
 class UserInfo {  String nickname;  int level;   UserInfo(this.nickname, this.level); }  class UserProvider extends ChangeNotifier {  UserInfo _userInfo = UserInfo("why", 18);   set userInfo(UserInfo info) {  _userInfo = info;  notifyListeners();  }   get userInfo {  return _userInfo;  } } 複製代碼

若是在開發中咱們有多個Provider須要提供應該怎麼作呢?

方式一:多個Provider之間嵌套

  • 這樣作有很大的弊端,若是嵌套層級過多不方便維護,擴展性也比較差
runApp(ChangeNotifierProvider(
 create: (context) => CounterProvider(),  child: ChangeNotifierProvider(  create: (context) => UserProvider(),  child: MyApp()  ),  )); 複製代碼

方式二:使用MultiProvider

runApp(MultiProvider(
 providers: [  ChangeNotifierProvider(create: (ctx) => CounterProvider()),  ChangeNotifierProvider(create: (ctx) => UserProvider()),  ],  child: MyApp(), )); 複製代碼

備註:全部內容首發於公衆號,以後除了Flutter也會更新其餘技術文章,TypeScript、React、Node、uniapp、mpvue、數據結構與算法等等,也會更新一些本身的學習心得等,歡迎你們關注

公衆號
公衆號
相關文章
相關標籤/搜索