Provider 5.0

  • 新的 create/update 回調函數是懶加載的, 也就是說他們在對應的值第一次被讀取時才被調用, 而非provider首次被創建時.

    如果你不需要這個特性, 你可以通過將provider的lazy屬性置為false, 來禁用懶加載

    FutureProvider(
      create: (_) async => doSomeHttpRequest(),
      lazy: false,
      child: ...
    )
    
  • ProviderNotFoundError 更名為 ProviderNotFoundException.

  • SingleChildCloneableWidget 接口被移除, 并被全新類型的組件 SingleChildWidget 所替代

    參考這個 issue 來獲取遷移細節.

  • Selector 現在會將先后的集合類型的值進行深層對比

    如果你不需要這個特性, 你可以通過 shouldRebuild 參數來使其還原至舊有表現.

    Selector<Selected, Consumed>(
      shouldRebuild: (previous, next) => previous == next,
      builder: ...,
    )
    
  • DelegateWidget及其家族widget被移除, 現在想要自定義provider, 直接繼承 InheritedProvider 或當前存在的provider.

使用

暴露一個值

暴露一個新的對象實例

Providers不僅允許暴露出一個值,也可以創建/監聽/銷毀它。

要暴露一個新創建的對象, 使用一個provider的默認構造函數. 如果你想創建一個對象, 不要使用 .value 構造函數, 否則可能會有你預期外的副作用。

查看該 StackOverflow Answer,來了解更多為什么不要使用.value構造函數創建值。

  • 在create內創建新對象

    Provider(
      create: (_) => MyModel(),
      child: ...
    )
    
  • 不要使用Provider.value創建對象

    ChangeNotifierProvider.value(
      value: MyModel(),
      child: ...
    )
    
  • 不要以可能隨時間改變的變量創建對象

    在這種情況下,如果變量發生變化,你的對象將永遠不會被更新

    int count;
    
    Provider(
      create: (_) => MyModel(count),
      child: ...
    )
    

    如果你想將隨時間改變的變量傳入給對象,請使用ProxyProvider:

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

注意:

在使用一個provider的create/update回調時,請注意回調函數默認是懶調用的。

也就是說, 除非這個值被讀取了至少一次, 否則create/update函數不會被調用。

如果你想預先計算一些邏輯, 可以通過使用lazy參數來禁用這一行為。

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

復用一個已存在的對象實例:

如果你已經擁有一個對象實例并且想暴露出它,你應當使用一個provider的.value構造函數。

如果你沒有這么做,那么在你調用對象的 dispose 方法時, 這個對象可能仍然在被使用。

  • 使用ChangeNotifierProvider.value來提供一個當前已存在的 ChangeNotifier

    MyChangeNotifier variable;
    
    ChangeNotifierProvider.value(
      value: variable,
      child: ...
    )
    
  • 不要使用默認的構造函數來嘗試復用一個已存在的 ChangeNotifier

    MyChangeNotifier variable;
    
    ChangeNotifierProvider(
      create: (_) => variable,
      child: ...
    )
    

讀取一個值

讀取一個值最簡單的方式就是使用BuildContext上的擴展屬性(由provider注入)。

  • context.watch<T>(), 一方法使得widget能夠監聽泛型T上發生的改變。
  • context.read<T>(),直接返回T,不會監聽改變。
  • context.select<T, R>(R cb(T value)),允許widget只監聽T上的一部分(R)。

或者使用 Provider.of<T>(context)這一靜態方法,它的表現類似 watch ,而在你為 listen 參數傳入 false 時(如 Provider.of<T>(context,listen: false) ),它的表現類似于 read

值得注意的是,context.read<T>() 方法不會在值變化時使得widget重新構建, 并且不能在 StatelessWidget.build/State.build 內調用. 換句話說, 它可以在除了這兩個方法以外的任意之處調用。

上面列舉的這些方法會與傳入的 BuildContext 關聯的widget開始查找widget樹,并返回查找到的最近的類型T的變量(如果沒有找到, 將拋出錯誤)。

值得注意是這一操作的復雜度是 O(1),它實際上并不涉及遍歷整個組件樹。

結合上面第一個向外暴露一個值的例子,這個widget會讀取暴露出的String并渲染Hello World

class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text(
      // Don't forget to pass the type of the object you want to obtain to `watch`!
      context.watch<String>(),
    );
  }
}

或者不使用這些方法,我們也可以使用 ConsumerSelector

這些往往在性能優化以及當很難獲取到provider的構建上下文后代(difficult to obtain a BuildContext descendant of the provider) 時是很有用的。

參見 FAQ 或關于ConsumerSelector 的文檔部分了解更多.

MultiProvider

當在大型應用中注入較多狀態時, Provider 很容易變得高度耦合:

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

使用MultiProvider:

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

以上兩個例子的實際表現是一致的, MultiProvider唯一改變的就是代碼書寫方式.

ProxyProvider

從3.0.0開始, 我們提供了一種新的provider: ProxyProvider.

ProxyProvider能夠將多個來自于其他的providers的值聚合為一個新對象,并且將結果傳遞給Provider

這個新對象會在其依賴的任一providers更新后被更新

下面的例子使用ProxyProvider,基于來自于另一個provider的counter值進行轉化。

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的數量

  • ProxyProvider vs ChangeNotifierProxyProvider vs ListenableProxyProvider, ...

    它們工作的方式是相似的, 但 ChangeNotifierProxyProvider 會將它的值傳遞給ChangeNotifierProvider 而非 Provider

FAQ

我是否能查看(inspect)我的對象的內容?

Flutter提供的開發者工具能夠展示特定時刻下的widget樹。

既然providers同樣是widget,他們同樣能通過開發者工具進行查看。

img

點擊一個provider, 即可查看它暴露出的值:

[圖片上傳失敗...(image-6c27fc-1623978187784)]

以上的開發者工具截圖來自于 /example 文件夾下的示例

開發者工具只顯示"Instance of MyClass", 我能做什么?

默認情況下, 開發者工具基于toString,也就使得默認結果是 "Instance of MyClass"。

如果要得到更多信息,你有兩種方式:

  • 使用Flutter提供的 Diagnosticable API

    在大多數情況下, 只需要在你的對象上使用 DiagnosticableTreeMixin 即可,以下是一個自定義 debugFillProperties 實現的例子:

    class MyClass with DiagnosticableTreeMixin {
      MyClass({this.a, this.b});
    
      final int a;
      final String b;
    
      @override
      void debugFillProperties(DiagnosticPropertiesBuilder properties) {
        super.debugFillProperties(properties);
        // list all the properties of your class here.
        // See the documentation of debugFillProperties for more information.
        properties.add(IntProperty('a', a));
        properties.add(StringProperty('b', b));
      }
    }
    
  • 重寫toString方法

    如果你無法使用 DiagnosticableTreeMixin (比如你的類在一個不依賴于Flutter的包中), 那么你可以通過簡單重寫toString方法來達成效果。

    這比使用 DiagnosticableTreeMixin 要更簡單,但能力也有著不足: 你無法 展開/折疊 來查看你的對象內部細節。

    class MyClass with DiagnosticableTreeMixin {
      MyClass({this.a, this.b});
    
      final int a;
      final String b;
    
      @override
      String toString() {
        return '$runtimeType(a: $a, b: $b)';
      }
    }
    

在獲得initState內部的Providers時發生了異常, 該做什么?

這個異常的出現是因為你在嘗試監聽一個來自于永遠不會再次被調用的生命周期的provider。

這意味著你要么使用另外一個生命周期(build),要么顯式指定你并不在意后續更新。

也就是說,不應該這么做:

initState() {
  super.initState();
  print(context.watch<Foo>().value);
}

你可以這么做:

Value value;

Widget build(BuildContext context) {
  final value = context.watch<Foo>.value;
  if (value != this.value) {
    this.value = value;
    print(value);
  }
}

這會且只會在value變化時打印它。

或者你也可以這么做:

initState() {
  super.initState();
  print(context.read<Foo>().value);
}

這樣只會打印一次value,并且會忽視后續的更新

如何控制我的對象上的熱更新?

你可以使你提供的對象實現 ReassembleHandler 類:

class Example extends ChangeNotifier implements ReassembleHandler {
  @override
  void reassemble() {
    print('Did hot-reload');
  }
}

通常會和 provider 一同使用:

ChangeNotifierProvider(create: (_) => Example()),

使用ChangeNotifier時, 在更新后出現了異常, 發生了什么?

這通常是因為你在widget樹正在構建時,從ChangeNotifier的某個后代更改了ChangeNotifier。

最典型的情況是在一個future被保存在notifier內部時發起http請求。

initState() {
  super.initState();
  context.read<MyNotifier>().fetchSomething();
}

這是不被允許的,因為更改會立即生效.

也就是說,一些widget可能在變更發生前構建,而有些則可能在變更后. 這可能造成UI不一致, 因此是被禁止的。

所以,你應該在一個整個widget樹所受影響相同的位置執行變更:

  • 直接在你的model的 provider/constructor 的 create 方法內調用:

    class MyNotifier with ChangeNotifier {
      MyNotifier() {
        _fetchSomething();
      }
    
      Future<void> _fetchSomething() async {}
    }
    

    在不需要傳入形參的情況下,這是相當有用的。

  • 在框架的末尾異步的執行(Future.microtask):

    initState() {
      super.initState();
      Future.microtask(() =>
        context.read<MyNotifier>(context).fetchSomething(someValue);
      );
    }
    

    這可能不是理想的使用方式,但它允許你向變更傳遞參數。

我必須為復雜狀態使用 ChangeNotifier 嗎?

不。

你可以使用任意對象來表示你的狀態,舉例來說,一個可選的架構方案是使用Provider.value配合StatefulWidget

這是一個使用這種架構的計數器示例:

class Example extends StatefulWidget {
  const Example({Key key, this.child}) : super(key: key);

  final Widget child;

  @override
  ExampleState createState() => ExampleState();
}

class ExampleState extends State<Example> {
  int _count;

  void increment() {
    setState(() {
      _count++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Provider.value(
      value: _count,
      child: Provider.value(
        value: this,
        child: widget.child,
      ),
    );
  }
}

我們可以通過這樣來讀取狀態:

return Text(context.watch<int>().toString());

并且這樣來修改狀態:

return FloatingActionButton(
  onPressed: () => context.read<ExampleState>().increment(),
  child: Icon(Icons.plus_one),
);

或者你還可以自定義provider.

我可以創建自己的Provider嗎?

可以,provider暴露出了所有構建功能完備的provider所需的組件,它包含:

  • SingleChildStatelessWidget, 使任意widget能夠與 MultiProvider 協作, 這個接口被暴露為包 package:provider/**single_child_widget 的一部分**
  • InheritedProvider,在使用 context.watch 時可獲取的通用InheritedWidget

這里有個使用 ValueNotifier 作為狀態的自定義provider例子:

https://gist.github.com/rrousselGit/4910f3125e41600df3c2577e26967c91

我的widget重構建太頻繁了, 我能做什么?

你可以使用 context.select 而非 context.watch 來指定只監聽對象的部分屬性:

舉例來說,你可以這么寫:

Widget build(BuildContext context) {
  final person = context.watch<Person>();
  return Text(person.name);
}

這可能導致widget在 name 以外的屬性發生變化時重構建。

你可以使用 context.select來 只監聽name屬性

Widget build(BuildContext context) {
  final name = context.select((Person p) => p.name);
  return Text(name);
}

這樣,這widget間就不會在name以外的屬性變化時進行不必要的重構建了。

同樣,你也可以使用Consumer/Selector,可選的child參數使得widget樹中只有所指定的一部分會重構建。

Foo(
  child: Consumer<A>(
    builder: (_, a, child) {
      return Bar(a: a, child: child);
    },
    child: Baz(),
  ),
)

在這個示例中, 只有Bar會在A更新時重構建,FooBaz不會進行不必要的重構建。

我能使用相同類型來獲得兩個不同的provider嗎?

不。 當你有兩個持有相同類型的不同provider時,一個widget只會獲取其中之一: 最近的一個

你必須顯式為兩個provider提供不同類型,而不是:

Provider<String>(
  create: (_) => 'England',
  child: Provider<String>(
    create: (_) => 'London',
    child: ...,
  ),
),

推薦的寫法:

Provider<Country>(
  create: (_) => Country('England'),
  child: Provider<City>(
    create: (_) => City('London'),
    child: ...,
  ),
),

我能消費一個接口并且提供一個實現嗎?

能,類型提示(type hint)必須被提供給編譯器,來指定將要被消費的接口,同時需要在craete中提供具體實現:

abstract class ProviderInterface with ChangeNotifier {
  ...
}

class ProviderImplementation with ChangeNotifier implements ProviderInterface {
  ...
}

class Foo extends StatelessWidget {
  @override
  build(context) {
    final provider = Provider.of<ProviderInterface>(context);
    return ...
  }
}

ChangeNotifierProvider<ProviderInterface>(
  create: (_) => ProviderImplementation(),
  child: Foo(),
),

現有的providers

provider中提供了幾種不同類型的"provider",供不同類型的對象使用。

完整的可用列表參見 provider-library

name description
Provider 最基礎的provider組成,接收一個值并暴露它, 無論值是什么。
ListenableProvider 供可監聽對象使用的特殊provider,ListenableProvider會監聽對象,并在監聽器被調用時更新依賴此對象的widgets。
ChangeNotifierProvider 為ChangeNotifier提供的ListenableProvider規范,會在需要時自動調用ChangeNotifier.dispose
ValueListenableProvider 監聽ValueListenable,并且只暴露出ValueListenable.value
StreamProvider 監聽流,并暴露出當前的最新值。
FutureProvider 接收一個Future,并在其進入complete狀態時更新依賴它的組件。
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,197評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,415評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,104評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,884評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,647評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,130評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,208評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,366評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,887評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,737評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,939評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,478評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,174評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,586評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,827評論 1 283
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,608評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,914評論 2 372

推薦閱讀更多精彩內容