重走Flutter狀態管理之路—Riverpod入門篇
熟悉我的朋友應該都知道,我好幾年前寫過一個「Flutter狀態管理之路」系列,那個時候介紹的是Provider,這也是官方推薦的狀態管理工具,但當時沒有寫完,因為寫著寫著,覺得有很多地方不盡人意,用著很彆扭,所以在寫了7篇文章之後,就暫時擱置了。
一晃時間過了這麼久,Flutter內部依然沒有一個能夠碾壓一切的狀態管理框架,GetX可能是,但是我覺得不是,InheritedWidget系的狀態管理,才應該是正統的狀態管理。
最近在留意Provider的後續進展時,意外發現了一個新的庫——Riverpod,號稱是新一代的狀態管理工具,仔細一看,嘿,居然還是Provider的作者,好傢伙,這是搬起石頭砸自己的腳啊。
就像作者所說,Riverpod就是對Provider的重寫,可不是嗎,字母都沒變,就換了個順序,這名字也是取的博大精深。
其實Provider在使用上已經非常不錯了,只不過隨著Flutter的更加深入,大家對它的需求也就越來越高,特別是對Provider中因為InheritedWidget層次問題導致的異常和BuildContext的使用這些問題詬病很多,而Riverpod,正是在Provider的基礎上,探索出了一條心的狀態管理之路。
大家可以先把官方文件看一看 http://riverpod.dev ,看完之後發現還是一臉懵逼,那就對了,Riverpod和Provider一樣,有很多型別的Provider,分別用於不同的場景,所以,理清這些Provider的不同作用和使用場景,對於我們用好Riverpod是非常有幫助的。
官網的文件,雖然是作者精心編寫的,但它的教程,站在的是一個創作者的角度,所以很多入門的初學者看上去會有點摸不清方向,所以,這才有了這個系列的文章。
我將在這個系列中,帶領大家對文件進行一次精讀,進行一次賞析,本文不全是對文件的翻譯,而且講解的順序也不一樣,所以,如果你想入門Riverpod進行狀態管理,那麼本文一定是你的最佳選擇。
Provider第一眼
首先,我們為什麼要進行狀態管理,狀態管理是解決申明式UI開發,關於資料狀態的一個處理操作,例如Widget A依賴於同級的Widget B的資料,那麼這個時候,就只能把資料狀態上提到它們的父類,但是這樣比較麻煩,Riverpod和Provider這樣的狀態管理框架,就是為了解決類似的問題而產生的。
將一個state包裹在一個Provider中可以有下面一些好處。
- 允許在多個位置輕鬆訪問該狀態。Provider可以完全替代Singletons、Service Locators、依賴注入或InheritedWidgets等模式
- 簡化了這個狀態與其他狀態的結合,你有沒有為,如何把多個物件合併成一個而苦惱過?這種場景可以直接在Provider內部實現
- 實現了效能優化。無論是過濾Widget的重建,還是快取昂貴的狀態計算;Provider確保只有受狀態變化影響的部分才被重新計算
- 增加了你的應用程式的可測試性。使用Provider,你不需要複雜的setUp/tearDown步驟。此外,任何Provider都可以被重寫,以便在測試期間有不同的行為,這可以輕鬆地測試一個非常具體的行為
- 允許與高階功能輕鬆整合,如logging或pull-to-refresh
首先,我們通過一個簡單的例子,來感受下,Riverpod是怎麼進行狀態管理的。
Provider是Riverpod應用程式中最重要的部分。Provider是一個物件,它封裝了一個state並允許監聽該state。Provider有很多變體形式,但它們的工作方式都是一樣的。
最常見的用法是將它們宣告為全域性常量,例如下面這樣。
final myProvider = Provider((ref) {
return MyValue();
});
不要被Provider的全域性變數所嚇倒。Provider是完全final的。宣告一個Provider與宣告一個函式沒有什麼不同,而且Provider是可測試和可維護的。
這段程式碼由三個部分組成。
- final myProvider,一個變數的宣告。這個變數是我們將來用來讀取我們Provider的狀態的。Provider應該始終是final的
- Provider,我們決定使用的Provider型別。Provider是所有Provider型別中最基本的。它暴露了一個永不改變的物件。我們可以用其他Provider如StreamProvider或StateNotifierProvider來替換Provider,以改變值的互動方式
- 一個建立共享狀態的函式。該函式將始終接收一個名為ref的物件作為引數。這個物件允許我們讀取其他Provider,在我們Provider的狀態將被銷燬時執行一些操作,以及其它一些事情
傳遞給Provider的函式返回的物件的型別,取決於所使用的Provider。例如,一個Provider的函式可以建立任何物件。另一方面,StreamProvider的回撥將被期望返回一個Stream。
你可以不受限制地宣告你想要的多個Provider。與使用package:provider不同的是,Riverpod允許建立多個暴露相同 "型別 "的狀態的provider。
final cityProvider = Provider((ref) => 'London'); final countryProvider = Provider((ref) => 'England');
兩個Provider都建立了一個字串,但這並沒有任何問題。
為了使Provider發揮作用,您必須在Flutter應用程式的根部新增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),
),
),
);
} } ```
可以發現,Riverpod的使用比package:Provider還要簡單,申明一個全域性變數來管理狀態資料,然後就可以在任意地方獲取資料了。
如何讀取Provider的狀態值
在有了一個簡單的瞭解後,我們先來了解下關於狀態中的「讀」。
在Riverpod中,我們不像package:Provider那樣需要依賴BuildContext,取而代之的是一個「ref」變數。這個東西,就是聯絡存取雙方的紐帶,這個物件允許我們與Provider互動,不管是來自一個Widget還是另一個Provider。
從Provider中獲取ref
所有Provider都有一個 "ref "作為引數。
``` final provider = Provider((ref) { // use ref to obtain other providers final repository = ref.watch(repositoryProvider);
return SomeValue(repository); }) ```
這個引數可以安全地傳遞給其它Provider或者類,來獲取所需要的值。
例如,一個常見的用例是將Provider的 "ref "傳遞給一個StateNotifier。
```
final counterProvider = StateNotifierProvider
class Counter extends StateNotifier
final Ref ref;
void increment() { // Counter can use the "ref" to read other providers final repository = ref.read(repositoryProvider); repository.post('...'); } } ```
這樣做,可以使我們的Counter類能夠讀取Provider。
這種方式是聯絡元件和Provider的一個重要方式。
從Widget中獲取ref
Widgets自然沒有一個ref引數。但是Riverpod提供了多種解決方案來從widget中獲得這個引數。
擴充套件ConsumerWidget
在widget樹中獲得一個ref的最常見的方法是用ConsumerWidget代替StatelessWidget。
ConsumerWidget在使用上與StatelessWidget相同,唯一的區別是它的構建方法上有一個額外的引數:"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相當於一個帶有狀態的StatefulWidget,不同的是,state有一個 "ref "物件。
這一次,"ref "不是作為構建方法的引數傳遞,而是作為ConsumerState物件的一個屬性。
``` class HomeView extends ConsumerStatefulWidget { const HomeView({Key? key}): super(key: key);
@override HomeViewState createState() => HomeViewState(); }
class HomeViewState extends ConsumerState
@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來獲取狀態
現在我們有了一個 "ref",我們可以開始使用它。
ref "有三個主要用途。
- 獲得一個Provider的值並監聽變化,這樣,當這個值發生變化時,這將重建訂閱該值的Widget或Provider。這是通過ref.watch完成的
- 在一個Provider上新增一個監聽器,以執行一個action,如導航到一個新的頁面或在該Provider發生變化時執行一些操作。這是通過 ref.listen 完成的
- 獲取一個Provider的值,同時忽略它的變化。當我們在一個事件中需要一個Provider的值時,這很有用,比如 "點選操作"。這是通過ref.read完成的
只要有可能,最好使用 ref.watch 而不是 ref.read 或 ref.listen 來實現一個功能。 通過依賴ref.watch,你的應用程式變得既是反應式的又是宣告式的,這使得它更容易維護。
通過ref.watch觀察Provider的狀態
ref.watch在Widget的構建方法中使用,或者在Provider的主體中使用,以使得Widget/Provider可以監聽另一個Provider。
例如,Provider可以使用 ref.watch 來將多個Provider合併成一個新的值。
一個例子是過濾一個todo-list,我們需要兩個Provider。
- filterTypeProvider,一個暴露當前過濾器型別的Provider(None,表示只顯示已完成的任務)
- todosProvider,一個暴露整個任務列表的Provider
通過使用ref.watch,我們可以製作第三個Provider,結合這兩個Provider來建立一個過濾後的任務列表。
```
final filterTypeProvider = StateProvider
final filteredTodoListProvider = Provider((ref) {
// obtains both the filter and the list of todos
final FilterType filter = ref.watch(filterTypeProvider);
final List
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現在就可以管理過濾後的任務列表。
如果過濾器或任務列表發生變化,過濾後的列表也會自動更新。同時,如果過濾器和任務列表都沒有改變,過濾後的列表將不會被重新計算。
類似地,一個Widget可以使用ref.watch來顯示來自Provider的內容,並在該內容發生變化時更新使用者介面。
``` 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,它監聽了一個儲存計數的Provider。如果該計數發生變化,該Widget將重建,使用者介面將更新以顯示新的值。
ref.watch方法不應該被非同步呼叫,比如在ElevatedButton的onPressed中。也不應該在initState和其他State的生命週期內使用它。在這些情況下,考慮使用 ref.read 來代替。
通過ref.listen監聽Provider的變化
與ref.watch類似,可以使用ref.listen來觀察一個Provider。
它們之間的主要區別是,如果被監聽的Provider發生變化,使用ref.listen不會重建widget/provider,而是會呼叫一個自定義函式。
這對於在某個變化發生時執行某些操作是很有用的,比如在發生錯誤時顯示一個snackbar。
ref.listen方法需要2個引數,第一個是Provider,第二個是當狀態改變時我們要執行的回撥函式。回撥函式在被呼叫時將被傳遞2個值,即先前狀態的值和新狀態的值。
ref.listen方法也可以在Provider的體內使用。
```
final counterProvider = StateNotifierProvider
final anotherProvider = Provider((ref) {
ref.listen
或在一個Widget的Build方法中使用。
```
final counterProvider = StateNotifierProvider
class HomeView extends ConsumerWidget { const HomeView({Key? key}): super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.listen
return Container();
} } ```
ref.listen也不應該被非同步呼叫,比如在ElevatedButton的onPressed中。也不應該在initState和其他State的生命週期內使用它。
通過ref.read來讀取Provider的狀態
ref.read方法是一種在不監聽的情況下獲取Provider的狀態的方法。
它通常用於由使用者互動觸發的函式中。例如,當用戶點選一個按鈕時,我們可以使用ref.read來增加一個計數器的值。
```
final counterProvider = StateNotifierProvider
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();
},
),
);
}
}
```
應該儘可能地避免使用ref.read,因為它不是響應式的。
它存在於使用watch或listen會導致問題的情況下。如果可以的話,使用watch/listen幾乎總是更好的,尤其是watch。
關於ref.read到底什麼時候用
首先,永遠不要在Widget的build函式中直接使用ref.read。
你可能很想使用ref.read來優化一個Widget的效能,例如通過下面的程式碼來實現。
``` 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'), ); } ```
但這是一種非常糟糕的做法,會導致難以追蹤的錯誤。
以這種方式使用 ref.read 通常與這樣的想法有關:"Provider所暴露的值永遠不會改變,所以使用'ref.read'是安全的"。這個假設的問題是,雖然今天該Provider可能確實從未更新過它的值,但不能保證明天也是如此。
軟體往往變化很大,而且很可能在未來,一個以前從未改變的值需要改變。
如果你使用ref.read,當這個值需要改變時,你必須翻閱整個程式碼庫,將ref.read改為ref.watch--這很容易出錯,而且你很可能會忘記一些情況。
如果你一開始就使用ref.watch,你在重構時就會減少問題。
但是如果我想用ref.read來減少我的widget重構的次數呢?
雖然這個目標值得稱讚,但需要注意的是,你可以用ref.watch代替來達到完全相同的效果(減少構建的次數)。
Provider提供了各種方法來獲得一個值,同時減少重建的次數,你可以用這些方法來代替。
例如下面的程式碼(bad)。
``` final counterProvider = StateProvider((ref) => 0);
Widget build(BuildContext context, WidgetRef ref) {
StateController
我們可以這樣改。
``` final counterProvider = StateProvider((ref) => 0);
Widget build(BuildContext context, WidgetRef ref) {
StateController
這兩個片段程式碼都達到了同樣的效果:當計數器增加時,我們的按鈕將不會重建。
另一方面,第二種方法支援計數器被重置的情況。例如,應用程式的另一部分可以呼叫。
ref.refresh(counterProvider);
這將重新建立StateController物件。
如果我們在這裡使用ref.read,我們的按鈕仍然會使用之前的StateController例項,而這個例項已經被棄置,不應該再被使用。
而使用ref.watch則可以正確地重建按鈕,使用新的StateController。
關於ref.read可以讀哪些值
根據你想監聽的Provider,你可能有多個可能的值可以監聽。
作為一個例子,考慮下面的StreamProvider。
final userProvider = StreamProvider<User>(...);
當讀取這個userProvider時,你可以像下面這樣。
- 通過監聽userProvider本身同步讀取當前狀態。
```
Widget build(BuildContext context, WidgetRef ref) {
AsyncValue
return user.when( loading: () => const CircularProgressIndicator(), error: (error, stack) => const Text('Oops'), data: (user) => Text(user.name), ); } ```
- 通過監聽userProvider.stream來獲得相關的Stream。
Widget build(BuildContext context, WidgetRef ref) {
Stream<User> user = ref.watch(userProvider.stream);
}
- 通過監聽userProvider.future獲得一個Future,該Future以最新發出的值進行解析。
Widget build(BuildContext context, WidgetRef ref) {
Future<User> user = ref.watch(userProvider.future);
}
其他Provider可能提供不同的替代值。
欲瞭解更多資訊,請查閱API參考資料,參考每個Provider的API文件。
通過select來控制精確的讀範圍
最後要提到的一個與讀取Provider有關的功能是,能夠減少Widget/Provider從ref.watch重建的次數,或者ref.listen執行函式的頻率的功能。
這一點很重要,因為預設情況下,監聽一個Provider會監聽整個物件的狀態。但有時,一個Widget/Provider可能只關心一些屬性的變化,而不是整個物件。
例如,一個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,當用戶的年齡發生變化時,這將重建widget。
解決方案是使用select來明確地告訴Riverpod我們只想監聽使用者的名字屬性。
更新後的程式碼將是這樣。
Widget build(BuildContext context, WidgetRef ref) {
String name = ref.watch(userProvider.select((user) => user.name));
return Text(name);
}
通過使用select,我們能夠指定一個函式來返回我們關心的屬性。
每當使用者改變時,Riverpod將呼叫這個函式並比較之前和新的結果。如果它們是不同的(例如當名字改變時),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'); } );
這樣做也將只在名稱改變時呼叫listener。
另外,你不一定要返回物件的一個屬性。任何覆蓋==的值都可以使用。例如,你可以這樣做。
final label = ref.watch(userProvider.select((user) => 'Mr ${user.name}'));
讀取狀態,是一個非常重要的部分,什麼時候用什麼樣的方式來讀,都會有不同的效果。
ProviderObserver
ProviderObserver可以監聽一個ProviderContainer的變化。
要使用它,你可以擴充套件ProviderObserver類並覆蓋你想使用的方法。ProviderObserver有三個方法。
- didAddProvider:在每次初始化一個Provider時被呼叫
- didDisposeProvider:在每次銷燬Provider的時候被呼叫
- didUpdateProvider:每次在Provider更新時都會被呼叫
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),
),
);
} } ```
現在,每當我們的Provider的值被更新時,logger將記錄它。
I/flutter (16783): {
I/flutter (16783): "provider": "counter",
I/flutter (16783): "newValue": "1"
I/flutter (16783): }
對於諸如StateController(StateProvider.state的狀態)和ChangeNotifier等可改變的狀態,previousValue和newValue將是相同的。因為它們引用的是同一個StateController / ChangeNotifier。
這些是對Riverpod的最基本瞭解,但是卻是很重要的部分,特別是如何對狀態值進行讀取,這是我們用好Riverpod的核心。
向大家推薦下我的網站 http://xuyisheng.top/ 專注 Android-Kotlin-Flutter 歡迎大家訪問
- 閒言碎語-第八期
- kotlin修煉指南9-Sequence的祕密
- 起點客戶端精準化測試的演進之路
- Flutter混編工程之打通紋理之路
- Android桌布還是B站玩得花
- Flutter佈局指南之誰動了我的Key
- Material Components——ShapeableImageView
- JetPack指路明燈—Navigation
- Material Components—預備役選手Transition
- 靜若處子動若脫兔-Constraintlayout2.0一探究竟
- Kotlin修煉指南5
- 重走Flutter狀態管理之路—Riverpod最終篇
- Material Components——MaterialButton
- ConstraintLayout2.0進階之路-歡迎新同學
- ConstraintLayout使用場景必知必會
- 重走Flutter狀態管理之路—Riverpod進階篇
- 它來了!Flutter3.0新特性全接觸
- 重走Flutter狀態管理之路—Riverpod入門篇
- 它來了!Flutter3.0釋出全解析
- Material Components——Shape的處理