iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 24
0
自我挑戰組

從零開始的Flutter世界系列 第 24

Day24 Flutter 的狀態管理 Provider (三)

  • 分享至 

  • xImage
  •  

上一篇介紹了Provider的基本原理與應用,今天我們來看看Provider.of,以及Provider的一些應用與類型吧

Provider.of

有的時候你不需要模型 ( 當初依要共享的狀態設計的 Model 類 ) 中的資料來改變UI,但是你可能還是需要使用該數據,比如,ClearCart按鈕能夠清空購物車的所有商品,它不需要顯示購物車裡的內容,只需要調用clear()方法

我們一樣可以使用Consumer<CartModel>來實現這個效果,不過這麼實現有點浪費,因為我們這樣在清空購物車時會重構了一個無需重構的widget,此時我們可以使用Provider.of,並且將listen設置為false,這樣就可以使用特定數據,又不會讓整體 UI 框架重構

例如:

Provider.of<CartModel>(context, listen: false).removeAll();

在build 方法中使用上面的代碼,當notifyListeners被調用的時候,並不會使widget 被重構

Provider.of<T>(context)Consumer常常會被拿來對比,二者到底有什麼差別呢,我們來看看Consumer的原始碼

@override
  Widget build(BuildContext context) {
    return builder(
      context,
      Provider.of<T>(context),
      child,
    );
  }

可以發現,Consumer就是通過Provider.of<T>(context)來實現的,但是它會將當數據發生變化後,把監聽者的widget 要重建的範圍限制地更小,而Provider.of<T>(context)將會把調用了該方法的context 作為觀察者,並在notifyListeners的時候通知其刷新 ( listen 預設都是true 除非去設定false ,不然一樣會去更新重建 )

結論:我們可以把Provider的使用分為兩種,數據資料改變的觸發者以及監聽/觀察者

  • 觸發者:如果只是要使用到所設計 Model 裡的資料,而不需要監聽資料的變化,例如上述的清空購物車按鈕,則建議使用Provider.of<Counter>(context, listen: false)來獲取資料
  • 監聽/觀察者:建議使用Consumer

熟悉Provider 的應用

參考文件,當前provider版本:provider 4.3.2

建立新的物件

如果我們想讓某個物件資料( 變量 ) 能夠被一個widget 以及其子widget 引用,我們可以使用Provider的建構create來建立所要的物件widget

例如:

Provider(
  create: (_) => MyModel(), //所要共享的物件資料
  child: ...
)

如果要將隨時間變化的變量傳遞給對象,請考慮使用ProxyProvider:

int count;

ProxyProvider0(
  update: (_, __) => MyModel(count),
  child: ...
)

當使用Providercreate/update回調時,需要注意的是,預設情況下create/updatelazy調用,也就是說,只有我們Provider中的數據至少被請求一次後,create/update才會被調用,如果我們想做一些預先處理,我們可以使用lazy參數來禁止這一特性

例如:

MyProvider(
  create: (_) => Something(),
  lazy: false,
)

重複使用已存在的物件

若想使用已建立的Provider 物件,則使用Provider的建構函數.value

若不是用建構函數.value使用已存在的Provider 物件可能會在物件仍在使用時,就被調用其dispose方法,使此Provider 物件被釋放資源 (dispose方法 後面會再介紹 )

例如:

MyChangeNotifier variable;

ChangeNotifierProvider.value(
  value: variable,
  child: ...
)

讀取Provider中的資料

最簡單的讀取資料的方式就是使用BuildContext的擴展方法:

  • context.watch<T>():讓widget 去監聽共享資料 T 的變化
  • context.read<T>():直接返回 T ,而不去監聽它的變化
  • context.select<T, R>(R cb(T value)):讓widget 可以只去監聽 T 的部分變化

當然也可以使用我們之前講的靜態方法Provider.of<T>(context),它和watch/read的行為很像,這也是在上面擴展方法出現之前,我們獲取數據的方式之一

這些方法將從傳遞過來的BuildContext相關的widget 開始查詢它的widget tree,並返回找到的符合類型 T 的最近變量 ( 如果未找到則拋出異常 )

範例:

class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text(
      // 不要忘記將想要獲取的對像類型傳遞給`watch`
      context.watch<String>(),
    );
  }
}

另外,也可以不使用這些方法,而可以使用ConsumerSelector,這些對於性能優化或在難以獲得Provider的BuildContext 後代很有用 (Selector會在後面介紹 )

dispose

typedef Disposer<T> = void Function(BuildContext context, T value);

Provider提供了dispose的回調,當Provider 所在節點被移除的時候,它就會啟動Disposer<T>,然後我們便可以在這裡釋放資源

在之前使用過BLoC 的經驗,我們遇到過一個問題,BloC 使用了觀察者模式,它旨在替代StatefulWidget,然而大量的流使用完畢之後必須close 掉,以釋放資源,但是應該在什麼時候釋放資源呢?而StatelessWidget並沒有類似於dispose 的方法,讓我們不得不為了釋放資源而使用StatefulWidget,然而Provider則為我們解決了這一點提供了dispose的回調

例如:

我們有一個 BLoC

class ExampleBLoC {
  StreamController<String> _data = StreamController<String>.broadcast();

  get data => _data.stream;

  doSomething(String text) {
    // ...
  }

  dispose() {
    _data.close();
  }
}

我們想要提供這個BLoC卻又不想使用StatefulWidget,此時可以透過Provider使用

Provider(
    create:(_) => ExampleBLoC(),
    dispose:(_, ExampleBLoC bloc) => bloc.dispose(), //在dispose 回調中關閉不再使用的流,即解決了資源釋放的問題    
)  

Selector

可參考文件

相當於Consumer,可以在特定值改變時,再去重新構建widget

例如,當List 長度改變時,才重新構建widget:

Selector<List, int>(
  selector: (_, list) => list.length,
  builder: (_, length, __) {
    return Text('$length');
  }
);

各種 Provider

MultiProvider

將多個Provider 合併成一個widget

Provider<Something>(
  create: (_) => Something(),
  child: Provider<SomethingElse>(
    create: (_) => SomethingElse(),
    child: Provider<AnotherThing>(
      create: (_) => AnotherThing(),
      child: someWidget,
    ),
  ),
),

變為:

MultiProvider(
  providers: [
    Provider<Something>(create: (_) => Something()),
    Provider<SomethingElse>(create: (_) => SomethingElse()),
    Provider<AnotherThing>(create: (_) => AnotherThing()),
  ],
  child: someWidget,
)
ProxyProvider

將多個provider 個別共享的值,整合成一個新對象,並將結果發送給外層的 provider,當所依賴的多個provider 的任一個發生變化時,此新對象都會更新

範例:

ProxyProvider来構建了一個依賴其他provider 提供的計數器的範例

Widget build(BuildContext context) {
  return MultiProvider(
    providers: [
      ChangeNotifierProvider(create: (_) => Counter()),
      ProxyProvider<Counter, Translations>(
        update: (_, counter, __) => Translations(counter.value),
      ),
    ],
    child: Foo(),
  );
}

class Translations {
  const Translations(this._value);

  final int _value;

  String get title => 'You clicked $_value times';
}

ProxyProvider vs ProxyProvider2 vs ProxyProvider3,… 類別名後面的數字是ProxyProvider依賴其他providers的數量

剩下還有幾種 Provider,就不一一介紹了,這邊來列個大概的種類

  • Provider:最基礎的provider,會獲取一個值並expose出來共享

  • ListenableProvider:用來共享可以監聽的對象,會隨著監聽對象的改變而更新widget

  • ChangeNotifierProviderListenableProviderChangeNotifierProvider其實是 父與子的關係ChangeNotifierProviderListenableProvider的基礎上,且可以在 Model 中覆寫 ChangeNotifier 的 dispose 方法,來釋放其資源,這對於複雜 Model 的情況下十分有用,在需要的時候能夠自動調用其 disposer 方法

  • ValueListenableProvider:要求builder返回的對象必須是ValueNotifier<T>的子類,T是需要共享的數據類型,用於處理只有一個單一變化數據的ChangeNotifier,通過ValueListenable 處理的類不再需要數據更新的時候調用notifyListeners

    ValueNotifier源碼:

    class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> {
      ValueNotifier(this._value);
    
      @override
      T get value => _value;
      T _value;
      set value(T newValue) {
        if (_value == newValue)
          return;
        _value = newValue;
        notifyListeners();
      }
    
      @override
      String toString() => '${describeIdentity(this)}($value)';
    }
    
  • StreamProvider:監聽一個流,並且expose 其最近發送的值

  • FutureProvider:提供了一個 Future 給其子孫節點,並在 Future 完成時,通知依賴的子孫節點進行刷新

今天就介紹到這邊,Provider 的內容也告一段落,已經完成我們對Provider的應用介紹,接下來我們將我們之前Android Studio 的登入範例,用Provider 改寫看看吧!


上一篇
Day23 Flutter 的狀態管理 Provider (二)
下一篇
Day25 Flutter 的狀態管理 Provider (四) Firebase Login
系列文
從零開始的Flutter世界30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言