-
新的 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
來提供一個當前已存在的 ChangeNotifierMyChangeNotifier 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>(),
);
}
}
或者不使用這些方法,我們也可以使用 Consumer 與 Selector。
這些往往在性能優化以及當很難獲取到provider的構建上下文后代(difficult to obtain a BuildContext
descendant of the provider) 時是很有用的。
參見 FAQ 或關于Consumer 和 Selector 的文檔部分了解更多.
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
vsProxyProvider2
vsProxyProvider3
, ...類名后的數字是
ProxyProvider
依賴的其他providers的數量 -
ProxyProvider
vsChangeNotifierProxyProvider
vsListenableProxyProvider
, ...它們工作的方式是相似的, 但
ChangeNotifierProxyProvider
會將它的值傳遞給ChangeNotifierProvider
而非Provider
。
FAQ
我是否能查看(inspect)我的對象的內容?
Flutter提供的開發者工具能夠展示特定時刻下的widget樹。
既然providers同樣是widget,他們同樣能通過開發者工具進行查看。
點擊一個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
更新時重構建,Foo
與Baz
不會進行不必要的重構建。
我能使用相同類型來獲得兩個不同的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狀態時更新依賴它的組件。 |