Flutter狀態管理:Provider4 入門教程(三)

JarvanMo發表於2020-07-12

前言

實在是抱歉,最近專案太忙,所以更新的太慢了。廢話不多說,我們開始吧。

Selector

讀文件

其實我本來是沒有計劃說說Selector的,但有朋友想讓我介紹一下,所以先從Selector開始。

總得來說,SelectorConsumer是等價的,也是通過Provider.of獲取資料的,不同的是,Selector正如他的名字一樣,他會過濾掉一些不必要的資料更新從而阻止重新構建,也就是說Selector只會更新符合條件的資料。

我們先看一下Selector的定義:

class Selector<A, S> extends Selector0<S> {
  /// {@macro provider.selector}
  Selector({
    Key key,
    @required ValueWidgetBuilder<S> builder,
    @required S Function(BuildContext, A) selector,
    ShouldRebuild<S> shouldRebuild,
    Widget child,
  })  : assert(selector != null),
        super(
          key: key,
          shouldRebuild: shouldRebuild,
          builder: builder,
          selector: (context) => selector(context, Provider.of(context)),
          child: child,
        );
}
複製程式碼

先解釋一下Selector<A, S>中的泛型:

  • A是我們從頂層獲取的Provider的型別
  • S是我們關心的具體型別,也就是獲取到的Provider中真正對我們有用的型別,需要在selector 中返回該型別。這個Selector的重新整理範圍也從整個Provider變成了 S。

快速地看一下Selector的中的屬性:

  • selector:就是一個Function,入參會將我們獲取的頂層 provider傳入,然後再返回我們所關心的S
  • shouldRebuild:這個屬性會儲存selector過濾後的值,也就是selector返回的S 並拿收到通知之後新的S與快取的S進行比較,以此來判斷這個Selector是否需要重新構建,預設preview!=next就重新整理,如果是collectionselector進行深度比較。
  • builder:和Consumer一樣,這裡返回的是要構建的控制元件,第二個引數provider,就是我們剛才selector中返回的S
  • child:這個用於優化一些不用重新整理的部分,之前我們說Consumer的時候也有說過。

預設情況下,Selector中的builder是否會被呼叫更新取決於selector中新舊資料比較結果,如果新舊資料是collection,那麼這個比較結果是通過collection包中的DeepCollectionEquality得出來的。

這個預設行為可以通過自定義shouldRebuild回撥來實現重寫。

注意:被選中的資料必須是不可變的(immutable),否則Selector可能會認為沒有任何變化,因此不會再次呼叫builder。

所以, selector應該返回的是一個集合(List/Map/Set/Iterable)或者重寫了==的類。

但是有時候我們並不想去重寫==,實現同樣效果最簡單的方式是使用Tuple:

Selector<Foo, Tuple2<Bar, Baz>>(
  selector: (_, foo) => Tuple2(foo.bar, foo.baz),
  builder: (_, data, __) {
    return Text('${data.item1}  ${data.item2}');
  }
)
複製程式碼

上面的例子中,只有foo.barfoo.bar發生變化時,builder才會被再次呼叫。

關於Tuple具體如何使用,大家可以自行學習。

舉個例子

上面說了一堆無非是對官方文件的羅列,我們說說具體應用。

簡單說一下我們要實現的功能,十分簡單,有一個商品列表,當我們點選某個商品的時候,商品會顯示加入購物車。這個功能其實很簡單了,我們需要為商品Commodity設定一個是否被加入購物車的欄位isSelected,然後當我們點選了商品時,我們要更新isSelected欄位,此時我們必然會通知Flutter更新UI,如果使用的是ChangeNotifier,那就是呼叫用notifyListeners。這可以實現我們的需求,但仔細一想,如果用這種方式,那麼所有依賴這個Provider的Commodity都會進行重新整理,也就全列表進行更新,這真的有必要嗎?

這個時候我們可以考慮使用Selector進行優化--過濾掉不必要的重新整理。

首先,我們建立一個CommodityProvider

class CommodityProvider with ChangeNotifier {
  List<Commodity> _commodityList =
      List.generate(10, (index) => Commodity('Commodity Name_$index', false));

  get commodityList => _commodityList;

  get length => commodityList.length;

  addToCart(int index) {
    Commodity commodity = commodityList[index];
    commodityList[index] = Commodity(commodity.name, !commodity.isSelected);
    notifyListeners();
  }
}
複製程式碼

Commodity這個實體類很簡單了,就兩個欄位,一個是商品的名字name,另一個是標記是否加入了購物車isSelected。其中_commodityList在實際工作中一般來說是從伺服器獲取的,這裡為了方便我們直接寫死。而addToCart方法則就是從購物車中加入或者刪除,當點選對應index的商品時,我們會將該商品新增到購物車或者從購物車中移除。我們要通過commodityList來渲染整個列表,而length則是商品列表的長度。

接下來我們要見證一下Selector是否真的可以過濾重新整理。 接下來,我們還是要在頂層頁面通過ChangeNotifierProvider提供資料。

class CommodityListScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_)=>CommodityProvider(),
      child: ourWidget,
    );
  }
}
複製程式碼

很顯然,要想實現這個列表我們必然得知道列表的長度length,而length是作用於整個列表的,但是我們並不希望它因為列表中某個商品發生變化就重新整理,所以現在我們要過濾掉全部重新整理,通過Selector實現一個不重新整理的“Consumer”。

    Selector<CommodityProvider, CommodityProvider>(
        shouldRebuild: (pre, next) => false,
        selector: (_, provider) => provider,
        builder: (context, provider, child) {
          print("build selector 1");
          return ourWidget;
        },
      ),
複製程式碼

在這裡,Selector中的泛型AS都是CommodityProvider,因為我們想要獲取的是整個CommodityProvider,只不過我們把shouldRebuild重寫了,從而避免不必要的重新整理。

接下來我們實現一下我們的商品列表:

ListView.builder(
              itemCount: provider.length,
              itemBuilder: (BuildContext context, int index) =>
                  Selector<CommodityProvider, Commodity>(
                    selector:
                        (BuildContext context, CommodityProvider provider) =>
                            provider.commodityList[index],
                    builder: (BuildContext context, Commodity commodity,
                        Widget child) {
                      print("build item $index");
                      return ListTile(
                        onTap: () => provider.addToCart(index),
                        title: Text("${commodity.name}"),
                        trailing: Icon(commodity.isSelected
                            ? Icons.remove_shopping_cart
                            : Icons.add_shopping_cart),
                      );
                    },
                  ));
        }
複製程式碼

我們可以看到這裡的selector返回了provider.commodityList[index],也就是某一個具體的商品,所以每個商品只需要關心自己的一畝三分地就OK了,這樣Selector的重新整理範圍就僅限於當前商品,與此同時我們在Selector<CommodityProvider, Commodity>builder裡新增了日誌以驗證過濾重新整理機制。

Come on!執行一下,隨便點幾個商品,然後看一下日誌:

I/flutter (29438): build selector 1
I/flutter (29438): build item 0
I/flutter (29438): build item 1
I/flutter (29438): build item 2
I/flutter (29438): build item 3
I/flutter (29438): build item 4
I/flutter (29438): build item 5
I/flutter (29438): build item 6
I/flutter (29438): build item 7
I/flutter (29438): build item 8
I/flutter (29438): build item 9
I/flutter (29438): build item 7
I/flutter (29438): build item 5
I/flutter (29438): build item 4
複製程式碼

怎麼樣?現在我們只重新整理了我們點選的商品,從而避免了整個列表的重新整理,我們又在效能優化的路上前進了一小步。

欲知後事請聽下回分解

作為Provider系列的第三篇,內容依然很簡單,而我又要說時間有限了。

未完待續。。。 期待不期待你說了算。

相關文章