重走Flutter狀態(tài)管理之路—Riverpod最終篇

點擊上方藍字關注我,知識會給你力量

最后一篇文章,我們在掌握了如何讀取狀態(tài)值,并知道如何根據不同場景選擇不同類型的Provider,以及如何對Provider進行搭配使用之后,再來了解一下它的一些其它特性,看看它們是如何幫助我們更好的進行狀態(tài)管理的。
Provider Modifiers
所有的Provider都有一個內置的方法來為你的不同Provider添加額外的功能。
它們可以為 ref 對象添加新的功能,或者稍微改變Provider的consume方式。Modifiers可以在所有Provider上使用,其語法類似于命名的構造函數。
final myAutoDisposeProvider = StateProvider.autoDispose<int>((ref) => 0);
final myFamilyProvider = Provider.family<String, int>((ref, id) => '$id');
目前,有兩個Modifiers可用。
.autoDispose,這將使Provider在不再被監(jiān)聽時自動銷毀其狀態(tài) .family,它允許使用一個外部參數創(chuàng)建一個Provider
一個Provider可以同時使用多個Modifiers。
final userProvider = FutureProvider.autoDispose.family<User, int>((ref, userId) async {
return fetchUser(userId);
});
.family
.family修飾符有一個目的:根據外部參數創(chuàng)建一個獨特的Provider。family的一些常見用例是下面這些。
將FutureProvider與.family結合起來,從其ID中獲取一個Message對象 將當前的Locale傳遞給Provider,這樣我們就可以處理國際化
family的工作方式是通過向Provider添加一個額外的參數。然后,這個參數可以在我們的Provider中自由使用,從而創(chuàng)建一些狀態(tài)。
例如,我們可以將family與FutureProvider結合起來,從其ID中獲取一個Message。
final messagesFamily = FutureProvider.family<Message, String>((ref, id) async {
return dio.get('http://my_api.dev/messages/$id');
});
當使用我們的 messagesFamily Provider時,語法會略有不同。
像下面這樣的通常語法將不再起作用。
Widget build(BuildContext context, WidgetRef ref) {
// Error – messagesFamily is not a provider
final response = ref.watch(messagesFamily);
}
相反,我們需要向 messagesFamily 傳遞一個參數。
Widget build(BuildContext context, WidgetRef ref) {
final response = ref.watch(messagesFamily('id'));
}
?我們可以同時使用一個具有不同參數的變量。
例如,我們可以使用titleFamily來同時讀取法語和英語的翻譯。
?@override
Widget build(BuildContext context, WidgetRef ref) {
final frenchTitle = ref.watch(titleFamily(const Locale('fr')));
final englishTitle = ref.watch(titleFamily(const Locale('en')));
return Text('fr: $frenchTitle en: $englishTitle');
}
參數限制
為了讓families正確工作,傳遞給Provider的參數必須具有一致的hashCode和==。
理想情況下,參數應該是一個基礎類型(bool/int/double/String),一個常數(Provider),或者一個重寫==和hashCode的不可變的對象。
?當參數不是常數時,更傾向于使用autoDispose
?
你可能想用family來傳遞一個搜索字段的輸入,給你的Provider。但是這個值可能會經常改變,而且永遠不會被重復使用。這可能導致內存泄漏,因為在默認情況下,即使不再使用,Provider也不會被銷毀。
同時使用.family和.autoDispose就可以修復這種內存泄漏。
final characters = FutureProvider.autoDispose.family<List<Character>, String>((ref, filter) async {
return fetchCharacters(filter: filter);
});
給family傳遞多重參數
family沒有內置支持向一個Provider傳遞多個值的方法。另一方面,這個值可以是任何東西(只要它符合前面提到的限制)。
這包括下面這些類型。
tuple類型,類似Python的元組,https://pub.dev/packages/tuple 用Freezed或build_value生成的對象,https://pub.dev/packages/freezed 使用equatable的對象,https://pub.dev/packages/equatable
下面是一個對多個參數使用Freezed或equatable的例子。
@freezed
abstract class MyParameter with _$MyParameter {
factory MyParameter({
required int userId,
required Locale locale,
}) = _MyParameter;
}
final exampleProvider = Provider.autoDispose.family<Something, MyParameter>((ref, myParameter) {
print(myParameter.userId);
print(myParameter.locale);
// Do something with userId/locale
});
@override
Widget build(BuildContext context, WidgetRef ref) {
int userId; // Read the user ID from somewhere
final locale = Localizations.localeOf(context);
final something = ref.watch(
exampleProvider(MyParameter(userId: userId, locale: locale)),
);
...
}
.autoDispose
它的一個常見的用例是,當一個Provider不再被使用時,要銷毀它的狀態(tài)。
這樣做的原因有很多,比如下面這些場景。
當使用Firebase時,要關閉連接并避免不必要的費用 當用戶離開一個屏幕并重新進入時,要重置狀態(tài)
Provider通過.autoDisposeModifiers內置了對這種使用情況的支持。
要告訴Riverpod當它不再被使用時銷毀一個Provider的狀態(tài),只需將.autoDispose附加到你的Provider上即可。
final userProvider = StreamProvider.autoDispose<User>((ref) {
});
就這樣了?,F在,userProvider的狀態(tài)將在不再使用時自動被銷毀。
注意通用參數是如何在autoDispose之后而不是之前傳遞的--autoDispose不是一個命名的構造函數。
如果需要,你可以將.autoDispose與其他Modifiers結合起來。
final userProvider = StreamProvider.autoDispose.family<User, String>((ref, id) {
});
ref.keepAlive
用autoDispose標記一個Provider時,也會在ref上增加了一個額外的方法:keepAlive。
keep函數是用來告訴Riverpod,即使不再被監(jiān)聽,Provider的狀態(tài)也應該被保留下來。
它的一個用例是在一個HTTP請求完成后,將這個標志設置為true。
final myProvider = FutureProvider.autoDispose((ref) async {
final response = await httpClient.get(...);
ref.keepAlive();
return response;
});
這樣一來,如果請求失敗,UI離開屏幕然后重新進入屏幕,那么請求將被再次執(zhí)行。但如果請求成功完成,狀態(tài)將被保留,重新進入屏幕將不會觸發(fā)新的請求。
示例:當Http請求不再使用時自動取消
autoDisposeModifiers可以與FutureProvider和ref.onDispose相結合,以便在不再需要HTTP請求時輕松取消。
我們的目標是:
當用戶進入一個屏幕時啟動一個HTTP請求 如果用戶在請求完成前離開屏幕,則取消HTTP請求 如果請求成功,離開并重新進入屏幕不會啟動一個新的請求
在代碼中,這將是下面這樣。
final myProvider = FutureProvider.autoDispose((ref) async {
// An object from package:dio that allows cancelling http requests
final cancelToken = CancelToken();
// When the provider is destroyed, cancel the http request
ref.onDispose(() => cancelToken.cancel());
// Fetch our data and pass our `cancelToken` for cancellation to work
final response = await dio.get('path', cancelToken: cancelToken);
// If the request completed successfully, keep the state
ref.keepAlive();
return response;
});
異常
當使用.autoDispose時,你可能會發(fā)現自己的應用程序無法編譯,出現類似下面的錯誤。
?The argument type 'AutoDisposeProvider' can't be assigned to the parameter type 'AlwaysAliveProviderBase'
?
不要擔心! 這個錯誤是正常的。它的發(fā)生是因為你很可能有一個bug。
例如,你試圖在一個沒有標記為.autoDispose的Provider中監(jiān)聽一個標記為.autoDispose的Provider,比如下面的代碼。
final firstProvider = Provider.autoDispose((ref) => 0);
final secondProvider = Provider((ref) {
// The argument type 'AutoDisposeProvider<int>' can't be assigned to the
// parameter type 'AlwaysAliveProviderBase<Object, Null>'
ref.watch(firstProvider);
});
這是不可取的,因為這將導致firstProvider永遠不會被dispose。
為了解決這個問題,可以考慮用.autoDispose標記secondProvider。
final firstProvider = Provider.autoDispose((ref) => 0);
final secondProvider = Provider.autoDispose((ref) {
ref.watch(firstProvider);
});
provider狀態(tài)關聯與整合
我們之前已經看到了如何創(chuàng)建一個簡單的Provider。但實際情況是,在很多情況下,一個Provider會想要讀取另一個Provider的狀態(tài)。
要做到這一點,我們可以使用傳遞給我們Provider的回調的ref對象,并使用其watch方法。
作為一個例子,考慮下面的Provider。
final cityProvider = Provider((ref) => 'London');
我們現在可以創(chuàng)建另一個Provider,它將消費我們的cityProvider。
final weatherProvider = FutureProvider((ref) async {
// We use `ref.watch` to listen to another provider, and we pass it the provider
// that we want to consume. Here: cityProvider
final city = ref.watch(cityProvider);
// We can then use the result to do something based on the value of `cityProvider`.
return fetchWeather(city: city);
});
這就是了。我們已經創(chuàng)建了一個依賴另一個Provider的Provider。
?這個其實在前面的例子中已經講到了,ref是可以連接多個不同的Provider的,這是Riverpod非常靈活的一個體現。
?
FAQ
What if the value being listened to changes over time?
根據你正在監(jiān)聽的Provider,獲得的值可能會隨著時間的推移而改變。例如,你可能正在監(jiān)聽一個StateNotifierProvider,或者被監(jiān)聽的Provider可能已經通過使用ProviderContainer.refresh/ref.refresh強制刷新。
當使用watch時,Riverpod能夠檢測到被監(jiān)聽的值發(fā)生了變化,并將在需要時自動重新執(zhí)行Provider的創(chuàng)建回調。
這對計算的狀態(tài)很有用。例如,考慮一個暴露了todo-list的StateNotifierProvider。
class TodoList extends StateNotifier<List<Todo>> {
TodoList(): super(const []);
}
final todoListProvider = StateNotifierProvider((ref) => TodoList());
一個常見的用例是讓用戶界面過濾todos的列表,只顯示已完成/未完成的todos。
實現這種情況的一個簡單方法是。
創(chuàng)建一個StateProvider,它暴露了當前選擇的過濾方法。
enum Filter {
none,
completed,
uncompleted,
}
final filterProvider = StateProvider((ref) => Filter.none);
做一個單獨的Provider,把過濾方法和todo-list結合起來,暴露出過濾后的todo-list。
final filteredTodoListProvider = Provider<List<Todo>>((ref) {
final filter = ref.watch(filterProvider);
final todos = ref.watch(todoListProvider);
switch (filter) {
case Filter.none:
return todos;
case Filter.completed:
return todos.where((todo) => todo.completed).toList();
case Filter.uncompleted:
return todos.where((todo) => !todo.completed).toList();
}
});
然后,我們的用戶界面可以監(jiān)聽filteredTodoListProvider來監(jiān)聽過濾后的todo-list。使用這種方法,當過濾器或todo-list發(fā)生變化時,用戶界面將自動更新。
要看到這種方法的作用,你可以看一下Todo List例子的源代碼。
?這種行為不是特定于Provider的,它適用于所有的Provider。
例如,你可以將watch與FutureProvider結合起來,實現一個支持實時配置變化的搜索功能。
// The current search filter
final searchProvider = StateProvider((ref) => '');
/// Configurations which can change over time
final configsProvider = StreamProvider<Configuration>(...);
final charactersProvider = FutureProvider<List<Character>>((ref) async {
final search = ref.watch(searchProvider);
final configs = await ref.watch(configsProvider.future);
final response = await dio.get('${configs.host}/characters?search=$search');
return response.data.map((json) => Character.fromJson(json)).toList();
});這段代碼將從服務中獲取一個字符列表,并在配置改變或搜索查詢改變時自動重新獲取該列表。
?
Can I read a provider without listening to it?
有時,我們想讀取一個Provider的內容,但在獲得的值發(fā)生變化時不需要重新創(chuàng)建值。
一個例子是一個 Repository,它從另一個Provider那里讀取用戶token用于認證。
我們可以使用觀察并在用戶token改變時創(chuàng)建一個新的 Repository,但這樣做幾乎沒有任何用處。
在這種情況下,我們可以使用read,這與listen類似,但不會導致Provider在獲得的值改變時重新創(chuàng)建它的值。
在這種情況下,一個常見的做法是將ref.read傳遞給創(chuàng)建的對象。然后,創(chuàng)建的對象將能夠隨時讀取Provider。
final userTokenProvider = StateProvider<String>((ref) => null);
final repositoryProvider = Provider((ref) => Repository(ref.read));
class Repository {
Repository(this.read);
/// The `ref.read` function
final Reader read;
Future<Catalog> fetchCatalog() async {
String token = read(userTokenProvider);
final response = await dio.get('/path', queryParameters: {
'token': token,
});
return Catalog.fromJson(response.data);
}
}
?你也可以把ref而不是ref.read傳給你的對象。
final repositoryProvider = Provider((ref) => Repository(ref));
class Repository {
Repository(this.ref);
final Ref ref;
}傳遞ref.read帶來的唯一區(qū)別是,它略微不那么冗長,并確保我們的對象永遠不會使用ref.watch。
?
但是,永遠不要像下面這樣做。
final myProvider = Provider((ref) {
// Bad practice to call `read` here
final value = ref.read(anotherProvider);
});
如果你使用read作為嘗試去避免太多的刷新重建,可以參考后面的FAQ
How to test an object that receives read as a parameter of its constructor?
如果你正在使用《我可以在不監(jiān)聽Provider的情況下讀取它嗎》中描述的模式,你可能想知道如何為你的對象編寫測試。
在這種情況下,考慮直接測試Provider而不是原始對象。你可以通過使用ProviderContainer類來做到這一點。
final repositoryProvider = Provider((ref) => Repository(ref.read));
test('fetches catalog', () async {
final container = ProviderContainer();
addTearOff(container.dispose);
Repository repository = container.read(repositoryProvider);
await expectLater(
repository.fetchCatalog(),
completion(Catalog()),
);
});
My provider updates too often, what can I do?
如果你的對象被重新創(chuàng)建得太頻繁,你的Provider很可能在監(jiān)聽它不關心的對象。
例如,你可能在監(jiān)聽一個配置對象,但只使用host屬性。
通過監(jiān)聽整個配置對象,如果host以外的屬性發(fā)生變化,這仍然會導致你的Provider被重新評估--這可能是不希望的。
這個問題的解決方案是創(chuàng)建一個單獨的Provider,只公開你在配置中需要的東西(所以是host)。
應當避免像下面的代碼一樣,對整個對象進行監(jiān)聽。
final configProvider = StreamProvider<Configuration>(...);
final productsProvider = FutureProvider<List<Product>>((ref) async {
// Will cause productsProvider to re-fetch the products if anything in the
// configurations changes
final configs = await ref.watch(configProvider.future);
return dio.get('${configs.host}/products');
});
當你只需要一個對象的單一屬性時,更應該使用select。
final configProvider = StreamProvider<Configuration>(...);
final productsProvider = FutureProvider<List<Product>>((ref) async {
// Listens only to the host. If something else in the configurations
// changes, this will not pointlessly re-evaluate our provider.
final host = await ref.watch(configProvider.selectAsync((config) => config.host));
return dio.get('$host/products');
});
這將只在host發(fā)生變化時重建 productsProvider。
通過這三篇文章,相信大家已經能熟練的對Riverpod進行使用了,相比package:Provider,Riverpod的使用更加簡單和靈活,這也是我推薦它的一個非常重要的原因,在入門之后,大家可以根據文檔中作者提供的示例來進行學習,充分的了解Riverpod在實戰(zhàn)中的使用技巧。
向大家推薦下我的網站 https://xuyisheng.top/ 點擊原文一鍵直達
專注 Android-Kotlin-Flutter 歡迎大家訪問
往期推薦
更文不易,點個“三連”支持一下??
