Provider 對比 Riverpod
本文將介紹 Provider 和 Riverpod 之間的差異和相似之處。
定義提供者程式
這兩個包之間的主要區別在於如何定義“提供者程式”。
對於 Provider,提供者程式是小部件,因此放置在小部件樹中,通常位於 MultiProvider
:
class Counter extends ChangeNotifier {
...
}
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider<Counter>(create: (context) => Counter()),
],
child: MyApp(),
)
);
}
使用 Riverpod,提供者程式不是小部件。相反,它們是普通的 Dart 物件。
同樣,提供者程式在小部件樹之外定義,而且宣告為全域性 final 變數。
此外,要使 Riverpod 正常工作,必須在整個應用程式上方新增一個小 ProviderScope
部件。
因此,使用 Riverpod 和 Provider 示例等效的版本為:
// provider 現在是頂級變數
final counterProvider = ChangeNotifierProvider<Counter>((ref) => Counter());
void main() {
runApp(
// 該小部件為整個專案啟用了 Riverpod
ProviderScope(
child: MyApp(),
),
);
}
請注意,這個 ChangeNotifierProvider 的定義只是向上移動了幾行。
由於 Riverpod 的提供者程式是普通的 Dart 物件,因此可以在沒有 Flutter 的情況下使用 Riverpod。
例如,Riverpod 可用於編寫命令列應用程式。
讀取提供者程式:使用 BuildContext
使用 Provider 庫,讀取提供者程式的一種方法是使用 Widget 的 BuildContext
。
例如,如果 provider 定義為:
Provider<Model>(...);
然後使用 Provider 讀取它就可以這樣做:
class Example extends StatelessWidget {
Widget build(BuildContext context) {
Model model = context.watch<Model>();
}
}
在 Riverpod 中的等效程式碼是:
final modelProvider = Provider<Model>(...);
class Example extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
Model model = ref.watch(modelProvider);
}
}
請注意:
- Riverpod 的程式碼片段是擴充套件
ConsumerWidget
的,而不是StatelessWidget
。 不同的小部件型別為我們的build
函式添加了一個額外的引數:WidgetRef
。
- 在 Riverpod 中我們使用
WidgetRef.watch
代替BuildContext.watch
,WidgetRef
是我們從ConsumerWidget
拿到的。
- Riverpod 不依賴於泛型型別。相反,它依賴於使用提供者程式定義建立的變數。
還要注意措辭的相似程度。Provider 和 Riverpod 都使用關鍵字“watch”來描述“當值更改時,這裡的小部件應重新生成”。
Riverpod 使用與 Provider 相同的術語來讀取提供者程式。
BuildContext.watch
->WidgetRef.watch
BuildContext.read
->WidgetRef.read
BuildContext.select
->WidgetRef.watch(myProvider.select)
context.watch
相對於 context.read
的規則也適用於 Riverpod:
在 build
方法中,使用 “watch”。在單擊處理程式和其他事件中,使用 “read”。
當需要過濾掉值並重新生成時,請使用 “select”。
讀取提供者程式:使用 Consumer
Provider 可以選擇附帶一個名為 Consumer
(以及名為 Consumer2
的變體)的小部件,用於讀取提供者程式。
Consumer
作為效能最佳化很有幫助,它允許對小部件樹進行更精細的重建 - 在狀態更改時僅更新相關的小部件:
因此,如果一個 provider 被定義為:
Provider<Model>(...);
Provider 允許使用 Consumer
讀取這個 provider:
Consumer<Model>(
builder: (BuildContext context, Model model, Widget? child) {
}
)
Riverpod 也有同樣的原理。Riverpod 也有一個以完全相同功能的 Consumer
小部件。
如果我們將一個 provider 定義為:
final modelProvider = Provider<Model>(...);
然後我們可以使用 Consumer
實現:
Consumer(
builder: (BuildContext context, WidgetRef ref, Widget? child) {
Model model = ref.watch(modelProvider);
}
)
注意 Consumer
是如何給我們一個 WidgetRef
物件。這與我們在上一部分中看到的 ConsumerWidget
與相關的物件相同。
Riverpod 中沒有 ConsumerN
等效的類
請注意,在 Riverpod 中不需要 pkg:Provider 的 Consumer2
、Consumer3
等,也不要遺漏重構它們。
使用 Riverpod,如果要從多個提供者程式讀取值,只需編寫多個 ref.watch 語句即可,如下所示:
Consumer(
builder: (context, ref, child) {
Model1 model = ref.watch(model1Provider);
Model2 model = ref.watch(model2Provider);
Model3 model = ref.watch(model3Provider);
// ...
}
)
與 pkg:Provider 的 ConsumerN
API 相比,上述解決方案感覺不那麼沉重,應該更容易理解。
組合提供者程式:ProxyProvider 與無狀態物件
使用 Provider 時,組合提供者程式的官方方法是使用 ProxyProvider
widget(或變體,例如 ProxyProvider2
)。
例如,我們可以定義:
class UserIdNotifier extends ChangeNotifier {
String? userId;
}
// ...
ChangeNotifierProvider<UserIdNotifier>(create: (context) => UserIdNotifier()),
在這裡我們有兩個選擇。我們可以組合 UserIdNotifier
建立一個新的“無狀態”提供者程式
(通常是一個可能覆蓋 == 的不可變值)。如:
ProxyProvider<UserIdNotifier, String>(
update: (context, userIdNotifier, _) {
return 'The user ID of the the user is ${userIdNotifier.userId}';
}
)
每當 UserIdNotifier.userId
發生更改時,這個提供者程式都會自動返回新的 String
值。
我們可以在 Riverpod 中做類似的事情,但語法不同。
首先,在 Riverpod 中,我們的 UserIdNotifier
定義是:
class UserIdNotifier extends ChangeNotifier {
String? userId;
}
// ...
final userIdNotifierProvider = ChangeNotifierProvider<UserIdNotifier>(
(ref) => UserIdNotifier(),
);
那樣的話,要基於 userId
生成 String
,我們可以這樣做:
final labelProvider = Provider<String>((ref) {
UserIdNotifier userIdNotifier = ref.watch(userIdNotifierProvider);
return 'The user ID of the the user is ${userIdNotifier.userId}';
});
請注意這行 ref.watch(userIdNotifierProvider) 做的事。
這行程式碼告訴 Riverpod 獲取 userIdNotifierProvider
的內容,
並且每當該值發生變化時, labelProvider
也會重新計算。
因此,每當 userId
更改時,
我們 labelProvider
發出的 String
都會自動更新。
這行 ref.watch
應該感覺很熟悉。
之前在解釋如何在小部件中讀取提供者程式時已經介紹了這個模式。
事實上,提供者程式現在能夠與小部件以相同的方式監聽其他提供者程式的改變。
組合提供者程式:ProxyProvider 與有狀態物件
組合提供者程式時,另一個替代用例是公開有狀態物件,例如 ChangeNotifier
例項。
為此,我們可以使用 ChangeNotifierProxyProvider
(或變體,例如 ChangeNotifierProxyProvider2
)。
例如,我們可以定義:
class UserIdNotifier extends ChangeNotifier {
String? userId;
}
// ...
ChangeNotifierProvider<UserIdNotifier>(create: (context) => UserIdNotifier()),
然後,我們可以定義基於 UserIdNotifier.userId
的一個 ChangeNotifier
。
例如,我們可以這樣做:
class UserNotifier extends ChangeNotifier {
String? _userId;
void setUserId(String? userId) {
if (userId != _userId) {
print('The user ID changed from $_userId to $userId');
_userId = userId;
}
}
}
// ...
ChangeNotifierProxyProvider<UserIdNotifier, UserNotifier>(
create: (context) => UserNotifier(),
update: (context, userIdNotifier, userNotifier) {
return userNotifier!
..setUserId(userIdNotifier.userId);
},
);
這個新提供者程式會建立一個 UserNotifier
例項(它永遠不會重新構造),
並在使用者 ID 更改時列印一個字串。
在提供者程式中執行相同的操作是以不同的方式實現的。
首先,在 Riverpod 中,我們的 UserIdNotifier
是這樣定義的:
class UserIdNotifier extends ChangeNotifier {
String? userId;
}
// ...
final userIdNotifierProvider = ChangeNotifierProvider<UserIdNotifier>(
(ref) => UserIdNotifier(),
),
相比於上面 ChangeNotifierProxyProvider
的等價程式碼將是:
class UserNotifier extends ChangeNotifier {
String? _userId;
void setUserId(String? userId) {
if (userId != _userId) {
print('The user ID changed from $_userId to $userId');
_userId = userId;
}
}
}
// ...
final userNotifierProvider = ChangeNotifierProvider<UserNotifier>((ref) {
final userNotifier = UserNotifier();
ref.listen<UserIdNotifier>(
userIdNotifierProvider,
(previous, next) {
if (previous?.userId != next.userId) {
userNotifier.setUserId(next.userId);
}
},
);
return userNotifier;
});
這個片段的核心是 ref.listen
這行程式碼。
這裡的 ref.listen
函式是一個實用程式,它允許監聽一個提供者程式,
並在提供者程式更改時執行函式。
該函式的 previous
和 next
引數對應於提供者程式更改前的最後一個值和更改後的新值。
作用域提供者程式與 .family
+ .autoDispose
在 pkg:Provider 中,作用域用於兩件事:
- 離開頁面時處置狀態
- 每頁具有自定義狀態
僅使用作用域來破壞狀態並不理想。
問題在於,作用域在大型應用程式上效果不佳。
例如,狀態通常在一個頁面中建立,但在導航後稍後在另一個頁面中處置。
這不允許多個快取在不同的頁面上處於活動狀態。
同樣,如果需要與小部件樹的另一部分共享狀態, “自定義每個頁面狀態”的方法很快就會變得難以處理, 就像你需要模態或多步驟表單一樣。
Riverpod 採取了不同的方法:首先,不鼓勵使用作用域提供者;
其次, .family
和 .autoDispose
是完整的替代解決方案。
在 Riverpod 中,當一個提供者程式標記為 .autoDispose
在不再使用時會自動處置的狀態。
當解除安裝最後一個刪除提供者程式的小部件時,Riverpod 將檢測到解除安裝並處置提供者程式。
嘗試在提供者程式中使用以下兩種生命週期方法來測試此行為:
ref.onCancel((){
print("我一個監聽程式都沒有了!");
});
ref.onDispose((){
print("如果我已經被定義為 `.autoDispose`,我將被處置!");
});
這從本質上解決了“破壞狀態”問題。
此外,還可以將提供者程式標記為 .family
(同時,也可以標記為 .autoDispose
)。
這樣就可以將引數傳遞給提供者程式,從而在內部生成和跟蹤多個提供者程式。
換句話說,在傳遞引數時,會為每個唯一引數建立一個唯一狀態。
int random(Ref ref, {required int seed, required int max}) {
return Random(seed).nextInt(max);
}
這解決了“每頁自定義狀態”問題。實際上,還有另一個優點:這種狀態不再繫結到一個特定的頁面。
相反,如果不同的頁面嘗試訪問相同的狀態,則該頁面只需重用引數即可實現。
在許多方面,將引數傳遞給提供者程式等同於 Map 的鍵。
如果鍵相同,則獲取的值相同。如果是不同的鍵,則將獲得不同的狀態。