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

點擊上方藍字關(guān)注我,知識會給你力量

熟悉我的朋友應(yīng)該都知道,我好幾年前寫過一個「Flutter狀態(tài)管理之路」系列,那個時候介紹的是Provider,這也是官方推薦的狀態(tài)管理工具,但當時沒有寫完,因為寫著寫著,覺得有很多地方不盡人意,用著很別扭,所以在寫了7篇文章之后,就暫時擱置了。
一晃時間過了這么久,F(xiàn)lutter內(nèi)部依然沒有一個能夠碾壓一切的狀態(tài)管理框架,GetX可能是,但是我覺得不是,InheritedWidget系的狀態(tài)管理,才應(yīng)該是正統(tǒng)的狀態(tài)管理。
最近在留意Provider的后續(xù)進展時,意外發(fā)現(xiàn)了一個新的庫——Riverpod,號稱是新一代的狀態(tài)管理工具,仔細一看,嘿,居然還是Provider的作者,好家伙,這是搬起石頭砸自己的腳啊。
就像作者所說,Riverpod就是對Provider的重寫,可不是嗎,字母都沒變,就換了個順序,這名字也是取的博大精深。
其實Provider在使用上已經(jīng)非常不錯了,只不過隨著Flutter的更加深入,大家對它的需求也就越來越高,特別是對Provider中因為InheritedWidget層次問題導(dǎo)致的異常和BuildContext的使用這些問題詬病很多,而Riverpod,正是在Provider的基礎(chǔ)上,探索出了一條心的狀態(tài)管理之路。
大家可以先把官方文檔看一看 https://riverpod.dev ,看完之后發(fā)現(xiàn)還是一臉懵逼,那就對了,Riverpod和Provider一樣,有很多類型的Provider,分別用于不同的場景,所以,理清這些Provider的不同作用和使用場景,對于我們用好Riverpod是非常有幫助的。
官網(wǎng)的文檔,雖然是作者精心編寫的,但它的教程,站在的是一個創(chuàng)作者的角度,所以很多入門的初學(xué)者看上去會有點摸不清方向,所以,這才有了這個系列的文章。
我將在這個系列中,帶領(lǐng)大家對文檔進行一次精讀,進行一次賞析,本文不全是對文檔的翻譯,而且講解的順序也不一樣,所以,如果你想入門Riverpod進行狀態(tài)管理,那么本文一定是你的最佳選擇。
Provider第一眼
首先,我們?yōu)槭裁匆M行狀態(tài)管理,狀態(tài)管理是解決申明式UI開發(fā),關(guān)于數(shù)據(jù)狀態(tài)的一個處理操作,例如Widget A依賴于同級的Widget B的數(shù)據(jù),那么這個時候,就只能把數(shù)據(jù)狀態(tài)上提到它們的父類,但是這樣比較麻煩,Riverpod和Provider這樣的狀態(tài)管理框架,就是為了解決類似的問題而產(chǎn)生的。
將一個state包裹在一個Provider中可以有下面一些好處。
允許在多個位置輕松訪問該狀態(tài)。Provider可以完全替代Singletons、Service Locators、依賴注入或InheritedWidgets等模式 簡化了這個狀態(tài)與其他狀態(tài)的結(jié)合,你有沒有為,如何把多個對象合并成一個而苦惱過?這種場景可以直接在Provider內(nèi)部實現(xiàn) 實現(xiàn)了性能優(yōu)化。無論是過濾Widget的重建,還是緩存昂貴的狀態(tài)計算;Provider確保只有受狀態(tài)變化影響的部分才被重新計算 增加了你的應(yīng)用程序的可測試性。使用Provider,你不需要復(fù)雜的setUp/tearDown步驟。此外,任何Provider都可以被重寫,以便在測試期間有不同的行為,這可以輕松地測試一個非常具體的行為 允許與高級功能輕松集成,如logging或pull-to-refresh
首先,我們通過一個簡單的例子,來感受下,Riverpod是怎么進行狀態(tài)管理的。
Provider是Riverpod應(yīng)用程序中最重要的部分。Provider是一個對象,它封裝了一個state并允許監(jiān)聽該state。Provider有很多變體形式,但它們的工作方式都是一樣的。
最常見的用法是將它們聲明為全局常量,例如下面這樣。
final myProvider = Provider((ref) {
return MyValue();
});
?不要被Provider的全局變量所嚇倒。Provider是完全final的。聲明一個Provider與聲明一個函數(shù)沒有什么不同,而且Provider是可測試和可維護的。
?
這段代碼由三個部分組成。
final myProvider,一個變量的聲明。這個變量是我們將來用來讀取我們Provider的狀態(tài)的。Provider應(yīng)該始終是final的 Provider,我們決定使用的Provider類型。Provider是所有Provider類型中最基本的。它暴露了一個永不改變的對象。我們可以用其他Provider如StreamProvider或StateNotifierProvider來替換Provider,以改變值的交互方式 一個創(chuàng)建共享狀態(tài)的函數(shù)。該函數(shù)將始終接收一個名為ref的對象作為參數(shù)。這個對象允許我們讀取其他Provider,在我們Provider的狀態(tài)將被銷毀時執(zhí)行一些操作,以及其它一些事情
傳遞給Provider的函數(shù)返回的對象的類型,取決于所使用的Provider。例如,一個Provider的函數(shù)可以創(chuàng)建任何對象。另一方面,StreamProvider的回調(diào)將被期望返回一個Stream。
?你可以不受限制地聲明你想要的多個Provider。與使用package:provider不同的是,Riverpod允許創(chuàng)建多個暴露相同 "類型 "的狀態(tài)的provider。
final cityProvider = Provider((ref) => 'London');
final countryProvider = Provider((ref) => 'England');兩個Provider都創(chuàng)建了一個字符串,但這并沒有任何問題。
?
為了使Provider發(fā)揮作用,您必須在Flutter應(yīng)用程序的根部添加ProviderScope。
void main() {
runApp(ProviderScope(child: MyApp()));
}
以上就是Riverpod最簡單的使用,我們看下完整的示例代碼。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// We create a "provider", which will store a value (here "Hello world").
// By using a provider, this allows us to mock/override the value exposed.
final helloWorldProvider = Provider((_) => 'Hello world');
void main() {
runApp(
// For widgets to be able to read providers, we need to wrap the entire
// application in a "ProviderScope" widget.
// This is where the state of our providers will be stored.
ProviderScope(
child: MyApp(),
),
);
}
// Extend ConsumerWidget instead of StatelessWidget, which is exposed by Riverpod
class MyApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final String value = ref.watch(helloWorldProvider);
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Example')),
body: Center(
child: Text(value),
),
),
);
}
}
可以發(fā)現(xiàn),Riverpod的使用比package:Provider還要簡單,申明一個全局變量來管理狀態(tài)數(shù)據(jù),然后就可以在任意地方獲取數(shù)據(jù)了。
如何讀取Provider的狀態(tài)值
在有了一個簡單的了解后,我們先來了解下關(guān)于狀態(tài)中的「讀」。
在Riverpod中,我們不像package:Provider那樣需要依賴BuildContext,取而代之的是一個「ref」變量。這個東西,就是聯(lián)系存取雙方的紐帶,這個對象允許我們與Provider互動,不管是來自一個Widget還是另一個Provider。
從Provider中獲取ref
所有Provider都有一個 "ref "作為參數(shù)。
final provider = Provider((ref) {
// use ref to obtain other providers
final repository = ref.watch(repositoryProvider);
return SomeValue(repository);
})
這個參數(shù)可以安全地傳遞給其它Provider或者類,來獲取所需要的值。
例如,一個常見的用例是將Provider的 "ref "傳遞給一個StateNotifier。
final counterProvider = StateNotifierProvider<Counter, int>((ref) {
return Counter(ref);
});
class Counter extends StateNotifier<int> {
Counter(this.ref): super(0);
final Ref ref;
void increment() {
// Counter can use the "ref" to read other providers
final repository = ref.read(repositoryProvider);
repository.post('...');
}
}
這樣做,可以使我們的Counter類能夠讀取Provider。
?這種方式是聯(lián)系組件和Provider的一個重要方式。
?
從Widget中獲取ref
Widgets自然沒有一個ref參數(shù)。但是Riverpod提供了多種解決方案來從widget中獲得這個參數(shù)。
擴展ConsumerWidget
在widget樹中獲得一個ref的最常見的方法是用ConsumerWidget代替StatelessWidget。
ConsumerWidget在使用上與StatelessWidget相同,唯一的區(qū)別是它的構(gòu)建方法上有一個額外的參數(shù):"ref "對象。
一個典型的ConsumerWidget看起來像這樣。
class HomeView extends ConsumerWidget {
const HomeView({Key? key}): super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
// use ref to listen to a provider
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
擴展ConsumerStatefulWidget
與ConsumerWidget類似,ConsumerStatefulWidget和ConsumerState相當于一個帶有狀態(tài)的StatefulWidget,不同的是,state有一個 "ref "對象。
這一次,"ref "不是作為構(gòu)建方法的參數(shù)傳遞,而是作為ConsumerState對象的一個屬性。
class HomeView extends ConsumerStatefulWidget {
const HomeView({Key? key}): super(key: key);
@override
HomeViewState createState() => HomeViewState();
}
class HomeViewState extends ConsumerState<HomeView> {
@override
void initState() {
super.initState();
// "ref" can be used in all life-cycles of a StatefulWidget.
ref.read(counterProvider);
}
@override
Widget build(BuildContext context) {
// We can also use "ref" to listen to a provider inside the build method
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
通過ref來獲取狀態(tài)
現(xiàn)在我們有了一個 "ref",我們可以開始使用它。
ref "有三個主要用途。
獲得一個Provider的值并監(jiān)聽變化,這樣,當這個值發(fā)生變化時,這將重建訂閱該值的Widget或Provider。這是通過ref.watch完成的 在一個Provider上添加一個監(jiān)聽器,以執(zhí)行一個action,如導(dǎo)航到一個新的頁面或在該Provider發(fā)生變化時執(zhí)行一些操作。這是通過 ref.listen 完成的 獲取一個Provider的值,同時忽略它的變化。當我們在一個事件中需要一個Provider的值時,這很有用,比如 "點擊操作"。這是通過ref.read完成的
?只要有可能,最好使用 ref.watch 而不是 ref.read 或 ref.listen 來實現(xiàn)一個功能。通過依賴ref.watch,你的應(yīng)用程序變得既是反應(yīng)式的又是聲明式的,這使得它更容易維護。
?
通過ref.watch觀察Provider的狀態(tài)
ref.watch在Widget的構(gòu)建方法中使用,或者在Provider的主體中使用,以使得Widget/Provider可以監(jiān)聽另一個Provider。
例如,Provider可以使用 ref.watch 來將多個Provider合并成一個新的值。
一個例子是過濾一個todo-list,我們需要兩個Provider。
filterTypeProvider,一個暴露當前過濾器類型的Provider(None,表示只顯示已完成的任務(wù)) todosProvider,一個暴露整個任務(wù)列表的Provider
通過使用ref.watch,我們可以制作第三個Provider,結(jié)合這兩個Provider來創(chuàng)建一個過濾后的任務(wù)列表。
final filterTypeProvider = StateProvider<FilterType>((ref) => FilterType.none);
final todosProvider = StateNotifierProvider<TodoList, List<Todo>>((ref) => TodoList());
final filteredTodoListProvider = Provider((ref) {
// obtains both the filter and the list of todos
final FilterType filter = ref.watch(filterTypeProvider);
final List<Todo> todos = ref.watch(todosProvider);
switch (filter) {
case FilterType.completed:
// return the completed list of todos
return todos.where((todo) => todo.isCompleted).toList();
case FilterType.none:
// returns the unfiltered list of todos
return todos;
}
});
有了這段代碼,filteredTodoListProvider現(xiàn)在就可以管理過濾后的任務(wù)列表。
如果過濾器或任務(wù)列表發(fā)生變化,過濾后的列表也會自動更新。同時,如果過濾器和任務(wù)列表都沒有改變,過濾后的列表將不會被重新計算。
類似地,一個Widget可以使用ref.watch來顯示來自Provider的內(nèi)容,并在該內(nèi)容發(fā)生變化時更新用戶界面。
final counterProvider = StateProvider((ref) => 0);
class HomeView extends ConsumerWidget {
const HomeView({Key? key}): super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
// use ref to listen to a provider
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
這段代碼顯示了一個Widget,它監(jiān)聽了一個存儲計數(shù)的Provider。如果該計數(shù)發(fā)生變化,該Widget將重建,用戶界面將更新以顯示新的值。
?ref.watch方法不應(yīng)該被異步調(diào)用,比如在ElevatedButton的onPressed中。也不應(yīng)該在initState和其他State的生命周期內(nèi)使用它。在這些情況下,考慮使用 ref.read 來代替。
?
通過ref.listen監(jiān)聽Provider的變化
與ref.watch類似,可以使用ref.listen來觀察一個Provider。
它們之間的主要區(qū)別是,如果被監(jiān)聽的Provider發(fā)生變化,使用ref.listen不會重建widget/provider,而是會調(diào)用一個自定義函數(shù)。
這對于在某個變化發(fā)生時執(zhí)行某些操作是很有用的,比如在發(fā)生錯誤時顯示一個snackbar。
ref.listen方法需要2個參數(shù),第一個是Provider,第二個是當狀態(tài)改變時我們要執(zhí)行的回調(diào)函數(shù)。回調(diào)函數(shù)在被調(diào)用時將被傳遞2個值,即先前狀態(tài)的值和新狀態(tài)的值。
ref.listen方法也可以在Provider的體內(nèi)使用。
final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter(ref));
final anotherProvider = Provider((ref) {
ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
print('The counter changed $newCount');
});
// ...
});
或在一個Widget的Build方法中使用。
final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter(ref));
class HomeView extends ConsumerWidget {
const HomeView({Key? key}): super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
print('The counter changed $newCount');
});
return Container();
}
}
?ref.listen也不應(yīng)該被異步調(diào)用,比如在ElevatedButton的onPressed中。也不應(yīng)該在initState和其他State的生命周期內(nèi)使用它。
?
通過ref.read來讀取Provider的狀態(tài)
ref.read方法是一種在不監(jiān)聽的情況下獲取Provider的狀態(tài)的方法。
它通常用于由用戶交互觸發(fā)的函數(shù)中。例如,當用戶點擊一個按鈕時,我們可以使用ref.read來增加一個計數(shù)器的值。
final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter(ref));
class HomeView extends ConsumerWidget {
const HomeView({Key? key}): super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
// Call `increment()` on the `Counter` class
ref.read(counterProvider.notifier).increment();
},
),
);
}
}
?應(yīng)該盡可能地避免使用ref.read,因為它不是響應(yīng)式的。
它存在于使用watch或listen會導(dǎo)致問題的情況下。如果可以的話,使用watch/listen幾乎總是更好的,尤其是watch。
?
關(guān)于ref.read到底什么時候用
首先,永遠不要在Widget的build函數(shù)中直接使用ref.read。
你可能很想使用ref.read來優(yōu)化一個Widget的性能,例如通過下面的代碼來實現(xiàn)。
final counterProvider = StateProvider((ref) => 0);
Widget build(BuildContext context, WidgetRef ref) {
// use "read" to ignore updates on a provider
final counter = ref.read(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}
但這是一種非常糟糕的做法,會導(dǎo)致難以追蹤的錯誤。
以這種方式使用 ref.read 通常與這樣的想法有關(guān):"Provider所暴露的值永遠不會改變,所以使用'ref.read'是安全的"。這個假設(shè)的問題是,雖然今天該Provider可能確實從未更新過它的值,但不能保證明天也是如此。
軟件往往變化很大,而且很可能在未來,一個以前從未改變的值需要改變。
如果你使用ref.read,當這個值需要改變時,你必須翻閱整個代碼庫,將ref.read改為ref.watch--這很容易出錯,而且你很可能會忘記一些情況。
如果你一開始就使用ref.watch,你在重構(gòu)時就會減少問題。
但是如果我想用ref.read來減少我的widget重構(gòu)的次數(shù)呢?
雖然這個目標值得稱贊,但需要注意的是,你可以用ref.watch代替來達到完全相同的效果(減少構(gòu)建的次數(shù))。
Provider提供了各種方法來獲得一個值,同時減少重建的次數(shù),你可以用這些方法來代替。
例如下面的代碼(bad)。
final counterProvider = StateProvider((ref) => 0);
Widget build(BuildContext context, WidgetRef ref) {
StateController<int> counter = ref.read(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}
我們可以這樣改。
final counterProvider = StateProvider((ref) => 0);
Widget build(BuildContext context, WidgetRef ref) {
StateController<int> counter = ref.watch(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}
這兩個片段代碼都達到了同樣的效果:當計數(shù)器增加時,我們的按鈕將不會重建。
另一方面,第二種方法支持計數(shù)器被重置的情況。例如,應(yīng)用程序的另一部分可以調(diào)用。
ref.refresh(counterProvider);
這將重新創(chuàng)建StateController對象。
如果我們在這里使用ref.read,我們的按鈕仍然會使用之前的StateController實例,而這個實例已經(jīng)被棄置,不應(yīng)該再被使用。
而使用ref.watch則可以正確地重建按鈕,使用新的StateController。
關(guān)于ref.read可以讀哪些值
根據(jù)你想監(jiān)聽的Provider,你可能有多個可能的值可以監(jiān)聽。
作為一個例子,考慮下面的StreamProvider。
final userProvider = StreamProvider<User>(...);
當讀取這個userProvider時,你可以像下面這樣。
通過監(jiān)聽userProvider本身同步讀取當前狀態(tài)。
Widget build(BuildContext context, WidgetRef ref) {
AsyncValue<User> user = ref.watch(userProvider);
return user.when(
loading: () => const CircularProgressIndicator(),
error: (error, stack) => const Text('Oops'),
data: (user) => Text(user.name),
);
}
通過監(jiān)聽userProvider.stream來獲得相關(guān)的Stream。
Widget build(BuildContext context, WidgetRef ref) {
Stream<User> user = ref.watch(userProvider.stream);
}
通過監(jiān)聽userProvider.future獲得一個Future,該Future以最新發(fā)出的值進行解析。
Widget build(BuildContext context, WidgetRef ref) {
Future<User> user = ref.watch(userProvider.future);
}
其他Provider可能提供不同的替代值。
欲了解更多信息,請查閱API參考資料,參考每個Provider的API文檔。
通過select來控制精確的讀范圍
最后要提到的一個與讀取Provider有關(guān)的功能是,能夠減少Widget/Provider從ref.watch重建的次數(shù),或者ref.listen執(zhí)行函數(shù)的頻率的功能。
這一點很重要,因為默認情況下,監(jiān)聽一個Provider會監(jiān)聽整個對象的狀態(tài)。但有時,一個Widget/Provider可能只關(guān)心一些屬性的變化,而不是整個對象。
例如,一個Provider可能暴露了一個User對象。
abstract class User {
String get name;
int get age;
}
但一個Widget可能只使用用戶名。
Widget build(BuildContext context, WidgetRef ref) {
User user = ref.watch(userProvider);
return Text(user.name);
}
如果我們簡單地使用ref.watch,當用戶的年齡發(fā)生變化時,這將重建widget。
解決方案是使用select來明確地告訴Riverpod我們只想監(jiān)聽用戶的名字屬性。
更新后的代碼將是這樣。
Widget build(BuildContext context, WidgetRef ref) {
String name = ref.watch(userProvider.select((user) => user.name));
return Text(name);
}
通過使用select,我們能夠指定一個函數(shù)來返回我們關(guān)心的屬性。
每當用戶改變時,Riverpod將調(diào)用這個函數(shù)并比較之前和新的結(jié)果。如果它們是不同的(例如當名字改變時),Riverpod將重建Widget。然而,如果它們是相等的(例如當年齡改變時),Riverpod將不會重建Widget。
?這個場景也可以使用select和ref.listen。
ref.listen<String>(
userProvider.select((user) => user.name),
(String? previousName, String newName) {
print('The user name changed $newName');
}
);這樣做也將只在名稱改變時調(diào)用listener。
另外,你不一定要返回對象的一個屬性。任何覆蓋==的值都可以使用。例如,你可以這樣做。
?final label = ref.watch(userProvider.select((user) => 'Mr ${user.name}'));
讀取狀態(tài),是一個非常重要的部分,什么時候用什么樣的方式來讀,都會有不同的效果。
ProviderObserver
ProviderObserver可以監(jiān)聽一個ProviderContainer的變化。
要使用它,你可以擴展ProviderObserver類并覆蓋你想使用的方法。ProviderObserver有三個方法。
didAddProvider:在每次初始化一個Provider時被調(diào)用 didDisposeProvider:在每次銷毀Provider的時候被調(diào)用 didUpdateProvider:每次在Provider更新時都會被調(diào)用
ProviderObserver的一個簡單用例是通過覆蓋didUpdateProvider方法來記錄Provider的變化。
// A Counter example implemented with riverpod with Logger
class Logger extends ProviderObserver {
@override
void didUpdateProvider(
ProviderBase provider,
Object? previousValue,
Object? newValue,
ProviderContainer container,
) {
print('''
{
"provider": "${provider.name ?? provider.runtimeType}",
"newValue": "$newValue"
}''');
}
}
void main() {
runApp(
// Adding ProviderScope enables Riverpod for the entire project
// Adding our Logger to the list of observers
ProviderScope(observers: [Logger()], child: const MyApp()),
);
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(home: Home());
}
}
final counterProvider = StateProvider((ref) => 0, name: 'counter');
class Home extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Scaffold(
appBar: AppBar(title: const Text('Counter example')),
body: Center(
child: Text('$count'),
),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(counterProvider.notifier).state++,
child: const Icon(Icons.add),
),
);
}
}
現(xiàn)在,每當我們的Provider的值被更新時,logger將記錄它。
I/flutter (16783): {
I/flutter (16783): "provider": "counter",
I/flutter (16783): "newValue": "1"
I/flutter (16783): }
?對于諸如StateController(StateProvider.state的狀態(tài))和ChangeNotifier等可改變的狀態(tài),previousValue和newValue將是相同的。因為它們引用的是同一個StateController / ChangeNotifier。
?
這些是對Riverpod的最基本了解,但是卻是很重要的部分,特別是如何對狀態(tài)值進行讀取,這是我們用好Riverpod的核心。
向大家推薦下我的網(wǎng)站 https://xuyisheng.top/ 點擊原文一鍵直達
專注 Android-Kotlin-Flutter 歡迎大家訪問
往期推薦
更文不易,點個“三連”支持一下??
