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

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

前面一篇文章,我們了解了如何正確的去讀取狀態(tài)值,這一篇,我們來了解下不同的Provider都有哪些使用場景。這篇文章,我們將真正的深入了解,如何在不同的場景下,選擇合適的種類的Provider,以及這些不同類型的Provider,都有哪些作用。
不同類型的Provider
Provider有多種類型的變種,可以用于多種不同的使用場景。
在所有這些Provider中,有時很難理解何時使用一種Provider類型而不是另一種。使用下面的表格,選擇一個適合你想提供給Widget樹的Provider。
| Provider Type | Provider Create Function | Example Use Case |
|---|---|---|
| Provider | Returns any type | A service class / computed property (filtered list) |
| StateProvider | Returns any type | A filter condition / simple state object |
| FutureProvider | Returns a Future of any type | A result from an API call |
| StreamProvider | Returns a Stream of any type | A stream of results from an API |
| StateNotifierProvider | Returns a subclass of StateNotifier | A complex state object that is immutable except through an interface |
| ChangeNotifierProvider | Returns a subclass of ChangeNotifier | A complex state object that requires mutability |
雖然所有的Provider都有他們的目的,但ChangeNotifierProviders不被推薦用于可擴展的應(yīng)用程序,因為它存在可變的狀態(tài)問題。它存在于flutter_riverpod包中,以提供一個簡單的從package:provider的遷移組件,并允許一些flutter特定的使用情況,如與一些Navigator 2包的集成。
Provider
Provider是所有Providers中最基本的。它返回了一個Value... 僅此而已。
Provider通常用于下面的場景。
緩存計算后的值 將一個值暴露給其他Provider(比如 Repository/HttpClient)提供了一個可供測試的覆寫Provider 通過不使用select,來減少Provider/widget的重建
通過Provider來對計算值進行緩存
當與ref.watch結(jié)合時,Provider是一個強大的工具,用于緩存同步操作。
一個典型的例子是過濾一個todos的列表。由于過濾一個列表的成本較高,我們最好不要在我們的應(yīng)用程序每次需要重新渲染的時候,就過濾一次我們的todos列表。在這種情況下,我們可以使用Provider來為我們做過濾工作。
為此,假設(shè)我們的應(yīng)用程序有一個現(xiàn)有的StateNotifierProvider,它管理一個todos列表。
class Todo {
Todo(this.description, this.isCompleted);
final bool isCompleted;
final String description;
}
class TodosNotifier extends StateNotifier<List<Todo>> {
TodosNotifier() : super([]);
void addTodo(Todo todo) {
state = [...state, todo];
}
// TODO add other methods, such as "removeTodo", ...
}
final todosProvider = StateNotifierProvider<TodosNotifier, List<Todo>>((ref) {
return TodosNotifier();
});
在這里,我們可以使用Provider來管理一個過濾后的todos列表,只顯示已完成的todos。
final completedTodosProvider = Provider<List<Todo>>((ref) {
// We obtain the list of all todos from the todosProvider
final todos = ref.watch(todosProvider);
// we return only the completed todos
return todos.where((todo) => todo.isCompleted).toList();
});
有了這段代碼,我們的用戶界面現(xiàn)在能夠通過監(jiān)聽 completedTodosProvider來顯示已完成的todos列表。
Consumer(builder: (context, ref, child) {
final completedTodos = ref.watch(completedTodosProvider);
// TODO show the todos using a ListView/GridView/...
});
有趣的是,現(xiàn)在的過濾后的列表是被緩存的。這意味著在添加/刪除/更新todos之前,已完成的todos列表不會被重新計算,即使我們多次讀取已完成的todos列表。
請注意,當todos列表發(fā)生變化時,我們不需要手動使緩存失效。由于有了ref.watch,Provider能夠自動知道何時必須重新計算結(jié)果。
通過Provider來減少provider/widget的重建
Provider的一個獨特之處在于,即使Provider被重新計算(通常在使用ref.watch時),它也不會更新監(jiān)聽它的widgets/providers,除非其值發(fā)生了變化。
一個真實的例子是啟用/禁用一個分頁視圖的上一個/下一個按鈕。

在我們的案例中,我們將特別關(guān)注 "上一頁 "按鈕。這種按鈕的一個普通的實現(xiàn),是一個獲得當前頁面索引的Widget,如果該索引等于0,我們將禁用該按鈕。
這段代碼可以是這樣。
final pageIndexProvider = StateProvider<int>((ref) => 0);
class PreviousButton extends ConsumerWidget {
const PreviousButton({Key? key}): super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
// if not on first page, the previous button is active
final canGoToPreviousPage = ref.watch(pageIndexProvider) != 0;
void goToPreviousPage() {
ref.read(pageIndexProvider.notifier).update((state) => state - 1);
}
return ElevatedButton(
onPressed: canGoToPreviousPage ? null : goToPreviousPage,
child: const Text('previous'),
);
}
}
這段代碼的問題是,每當我們改變當前頁面時,"上一頁 "按鈕就會重新Build。在理想的世界里,我們希望這個按鈕只在激活和停用之間變化時才重新build。
這里問題的根源在于,我們正在計算用戶是否被允許在 "上一頁 "按鈕中直接轉(zhuǎn)到上一頁。
解決這個問題的方法是把這個邏輯從widget中提取出來,放到一個Provider中。
final pageIndexProvider = StateProvider<int>((ref) => 0);
// A provider which computes whether the user is allowed to go to the previous page
final canGoToPreviousPageProvider = Provider<bool>((ref) {
return ref.watch(pageIndexProvider) != 0;
});
class PreviousButton extends ConsumerWidget {
const PreviousButton({Key? key}): super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
// We are now watching our new Provider
// Our widget is no longer calculating whether we can go to the previous page.
final canGoToPreviousPage = ref.watch(canGoToPreviousPageProvider);
void goToPreviousPage() {
ref.read(pageIndexProvider.notifier).update((state) => state - 1);
}
return ElevatedButton(
onPressed: canGoToPreviousPage ? null : goToPreviousPage,
child: const Text('previous'),
);
}
}
通過這個小的重構(gòu),我們的PreviousButton Widget將不會在頁面索引改變時重建,這都要歸功于Provider的緩存作用。
從現(xiàn)在開始,當頁面索引改變時,我們的canGoToPreviousPageProviderProvider將被重新計算。但是如果Provider暴露的值沒有變化,那么PreviousButton將不會重建。
這個變化既提高了我們的按鈕的性能,又有一個有趣的好處,就是把邏輯提取到我們的Widget之外。
StateProvider
我們再來看下StateProvider,它是一個公開了修改其狀態(tài)的方法的Provider。它是StateNotifierProvider的簡化版,旨在避免為非常簡單的用例編寫一個StateNotifier類。
StateProvider的存在主要是為了允許用戶對簡單的變量進行修改。一個StateProvider所維護的狀態(tài)通常是下面幾種。
一個枚舉,比如一個filter,用來做篩選 一個字符串,通常是一些固定的文本,可以借助family關(guān)鍵字來做Switch 一個布爾值,用于checkbox這類的狀態(tài)切換 一個數(shù)字,用于分頁或者Pager的Index
而下面這些場景,就不適合使用StateProvider。
你的狀態(tài)中包含對校驗邏輯 你的狀態(tài)是一個復(fù)雜的對象,比如一個自定義類,一個List、Map等 狀態(tài)的修改邏輯比較復(fù)雜
對于這些場景,你可以考慮使用StateNotifierProvider代替,并創(chuàng)建一個StateNotifier類。
雖然StateNotifierProvider的模板代碼會多一些,但擁有一個自定義的StateNotifier類對于項目的長期可維護性至關(guān)重要--因為它將你的狀態(tài)的業(yè)務(wù)邏輯集中在一個地方。
由此,我們可以了解,Riverpod最合適的場景,就是「單一狀態(tài)值的管理」。例如,PageView的切換Index、ListView的切換Index,或者是CheckBox、dropdown的內(nèi)容改變監(jiān)聽,這些是非常適合用StateProvider的。
一個filter的示例
官方給出了一個dropdown的例子,用來演示如何根據(jù)filter來修改列表的排序。
StateProvider在現(xiàn)實世界中的一個使用案例是管理簡單表單組件的狀態(tài),如dropdown/text fields/checkboxes。特別是,我們將看到如何使用StateProvider來實現(xiàn)一個允許改變產(chǎn)品列表排序方式的dropdown。為了簡單起見,我們將獲得的產(chǎn)品列表將直接在應(yīng)用程序中建立,其內(nèi)容如下。
class Product {
Product({required this.name, required this.price});
final String name;
final double price;
}
final _products = [
Product(name: 'iPhone', price: 999),
Product(name: 'cookie', price: 2),
Product(name: 'ps5', price: 500),
];
final productsProvider = Provider<List<Product>>((ref) {
return _products;
});
在現(xiàn)實世界的應(yīng)用中,這個列表通常是通過使用FutureProvider進行網(wǎng)絡(luò)請求來獲得的,然后,用戶界面可以顯示產(chǎn)品列表,就像下面這樣。
Widget build(BuildContext context, WidgetRef ref) {
final products = ref.watch(productsProvider);
return Scaffold(
body: ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return ListTile(
title: Text(product.name),
subtitle: Text('${product.price} \$'),
);
},
),
);
}
由于這里是寫死了products,所以使用Provider來作為數(shù)據(jù)Provider,是一個很好的選擇。
現(xiàn)在我們已經(jīng)完成了基礎(chǔ)框架,我們可以添加一個dropdown,這將允許我們通過價格或名稱來過濾產(chǎn)品。為此,我們將使用DropDownButton。
// An enum representing the filter type
enum ProductSortType {
name,
price,
}
Widget build(BuildContext context, WidgetRef ref) {
final products = ref.watch(productsProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Products'),
actions: [
DropdownButton<ProductSortType>(
value: ProductSortType.price,
onChanged: (value) {},
items: const [
DropdownMenuItem(
value: ProductSortType.name,
child: Icon(Icons.sort_by_alpha),
),
DropdownMenuItem(
value: ProductSortType.price,
child: Icon(Icons.sort),
),
],
),
],
),
body: ListView.builder(
// ...
),
);
}
現(xiàn)在我們有了一個dropdown,讓我們創(chuàng)建一個StateProvider并將dropdown的狀態(tài)與我們的StateProvider同步。首先,讓我們創(chuàng)建StateProvider。
final productSortTypeProvider = StateProvider<ProductSortType>(
// We return the default sort type, here name.
(ref) => ProductSortType.name,
);
然后我們可以通過下面這個方式,將StateProvider和dropdown聯(lián)系起來。
DropdownButton<ProductSortType>(
// When the sort type changes, this will rebuild the dropdown
// to update the icon shown.
value: ref.watch(productSortTypeProvider),
// When the user interacts with the dropdown, we update the provider state.
onChanged: (value) =>
ref.read(productSortTypeProvider.notifier).state = value!,
items: [
// ...
],
),
有了這個,我們現(xiàn)在應(yīng)該能夠改變排序類型。不過,這對產(chǎn)品列表還沒有影響。現(xiàn)在是最后一個部分了。更新我們的productsProvider來對產(chǎn)品列表進行排序。
實現(xiàn)這一點的一個關(guān)鍵部分是使用ref.watch,讓我們的productProvider獲取排序類型,并在排序類型改變時重新計算產(chǎn)品列表。實現(xiàn)的方法如下。
final productsProvider = Provider<List<Product>>((ref) {
final sortType = ref.watch(productSortTypeProvider);
switch (sortType) {
case ProductSortType.name:
return _products.sorted((a, b) => a.name.compareTo(b.name));
case ProductSortType.price:
return _products.sorted((a, b) => a.price.compareTo(b.price));
}
});
這就是全部代碼,這一改變足以讓用戶界面在排序類型改變時自動重新對產(chǎn)品列表進行排序。
更新狀態(tài)的簡化
參考下面的這個場景,有時候,我們需要根據(jù)前一個狀態(tài)值,來修改后續(xù)的狀態(tài)值,例如Flutter Demo中的加數(shù)器。
final counterProvider = StateProvider<int>((ref) => 0);
class HomeView extends ConsumerWidget {
const HomeView({Key? key}): super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
// We're updating the state from the previous value, we ended-up reading
// the provider twice!
ref.read(counterProvider.notifier).state = ref.read(counterProvider.notifier).state + 1;
},
),
);
}
}
這種更新State的方法,我們可以使用update函數(shù)來簡化,簡化之后,代碼如下。
ref.read(counterProvider.notifier).update((state) => state + 1);
所以,如果是對StateProvider的state進行賦值,那么直接使用下面的代碼即可。
ref.read(counterProvider.notifier).state = xxxx
那么如果是根據(jù)前置狀態(tài)的值來修改狀態(tài)值,則可以使用update來簡化。
StateNotifierProvider
StateNotifierProvider是一個用于監(jiān)聽和管理StateNotifier的Provider。StateNotifierProvider和StateNotifier是Riverpod推薦的解決方案,用于管理可能因用戶交互而改變的狀態(tài)。
它通常被用于下面這些場景。
暴露一個不可變的,跟隨時間和行為而發(fā)生改變的狀態(tài) 將修改某些狀態(tài)的邏輯(又稱 "業(yè)務(wù)邏輯")集中在一個地方,提高長期的可維護性
作為一個使用例子,我們可以使用StateNotifierProvider來實現(xiàn)一個todo-list。這樣做可以讓我們暴露出諸如addTodo這樣的方法,讓UI在用戶交互中修改todos列表。
// The state of our StateNotifier should be immutable.
// We could also use packages like Freezed to help with the implementation.
@immutable
class Todo {
const Todo({required this.id, required this.description, required this.completed});
// All properties should be `final` on our class.
final String id;
final String description;
final bool completed;
// Since Todo is immutable, we implement a method that allows cloning the
// Todo with slightly different content.
Todo copyWith({String? id, String? description, bool? completed}) {
return Todo(
id: id ?? this.id,
description: description ?? this.description,
completed: completed ?? this.completed,
);
}
}
// The StateNotifier class that will be passed to our StateNotifierProvider.
// This class should not expose state outside of its "state" property, which means
// no public getters/properties!
// The public methods on this class will be what allow the UI to modify the state.
class TodosNotifier extends StateNotifier<List<Todo>> {
// We initialize the list of todos to an empty list
TodosNotifier(): super([]);
// Let's allow the UI to add todos.
void addTodo(Todo todo) {
// Since our state is immutable, we are not allowed to do `state.add(todo)`.
// Instead, we should create a new list of todos which contains the previous
// items and the new one.
// Using Dart's spread operator here is helpful!
state = [...state, todo];
// No need to call "notifyListeners" or anything similar. Calling "state ="
// will automatically rebuild the UI when necessary.
}
// Let's allow removing todos
void removeTodo(String todoId) {
// Again, our state is immutable. So we're making a new list instead of
// changing the existing list.
state = [
for (final todo in state)
if (todo.id != todoId) todo,
];
}
// Let's mark a todo as completed
void toggle(String todoId) {
state = [
for (final todo in state)
// we're marking only the matching todo as completed
if (todo.id == todoId)
// Once more, since our state is immutable, we need to make a copy
// of the todo. We're using our `copyWith` method implemented before
// to help with that.
todo.copyWith(completed: !todo.completed)
else
// other todos are not modified
todo,
];
}
}
// Finally, we are using StateNotifierProvider to allow the UI to interact with
// our TodosNotifier class.
final todosProvider = StateNotifierProvider<TodosNotifier, List<Todo>>((ref) {
return TodosNotifier();
});
現(xiàn)在我們已經(jīng)定義了一個StateNotifierProvider,我們可以用它來與用戶界面中的todos列表進行交互。
class TodoListView extends ConsumerWidget {
const TodoListView({Key? key}): super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
// rebuild the widget when the todo list changes
List<Todo> todos = ref.watch(todosProvider);
// Let's render the todos in a scrollable list view
return ListView(
children: [
for (final todo in todos)
CheckboxListTile(
value: todo.completed,
// When tapping on the todo, change its completed status
onChanged: (value) => ref.read(todosProvider.notifier).toggle(todo.id),
title: Text(todo.description),
),
],
);
}
}
FutureProvider
FutureProvider相當于Provider,但僅用于異步代碼。
FutureProvider通常用于下面這些場景。
執(zhí)行和緩存異步操作(如網(wǎng)絡(luò)請求) 更好地處理異步操作的錯誤、加載狀態(tài) 將多個異步值合并為另一個值
FutureProvider在與ref.watch結(jié)合時收獲頗豐。這種組合允許在一些變量發(fā)生變化時自動重新獲取一些數(shù)據(jù),確保我們始終擁有最新的值。
?FutureProvider不提供在用戶交互后直接修改計算的方法。它被設(shè)計用來解決簡單的用例。
對于更高級的場景,可以考慮使用StateNotifierProvider。
?
示例:讀取一個配置文件
FutureProvider可以作為一種方便的方式來管理一個通過讀取JSON文件創(chuàng)建的配置對象。
創(chuàng)建配置將用典型的async/await語法完成,但在Provider內(nèi)部。使用Flutter的asset,這將是下面的代碼。
final configProvider = FutureProvider<Configuration>((ref) async {
final content = json.decode(
await rootBundle.loadString('assets/configurations.json'),
) as Map<String, Object?>;
return Configuration.fromJson(content);
});
然后,用戶界面可以像這樣監(jiān)聽配置。
Widget build(BuildContext context, WidgetRef ref) {
AsyncValue<Configuration> config = ref.watch(configProvider);
return config.when(
loading: () => const CircularProgressIndicator(),
error: (err, stack) => Text('Error: $err'),
data: (config) {
return Text(config.host);
},
);
}
這將在Future完成后自動重建UI。同時,如果多個widget想要這些解析值,asset將只被解碼一次。
正如你所看到的,監(jiān)聽Widget內(nèi)的FutureProvider會返回一個AsyncValue - 它允許處理錯誤/加載狀態(tài)。
StreamProvider
StreamProvider類似于FutureProvider,但用于Stream而不是Future。
StreamProvider通常被用于下面這些場景。
監(jiān)聽Firebase或web-sockets 每隔幾秒鐘重建另一個Provider
由于Streams自然地暴露了一種監(jiān)聽更新的方式,有些人可能認為使用StreamProvider的價值很低。特別是,你可能認為Flutter的StreamBuilder也能很好地用于監(jiān)聽Stream,但這是一個錯誤。
使用StreamProvider而不是StreamBuilder有許多好處。
它允許其他Provider使用ref.watch來監(jiān)聽Stream 由于AsyncValue的存在,它可以確保加載和錯誤情況得到正確處理 它消除了區(qū)分broadcast streams和normal stream的需要 它緩存了stream所發(fā)出的最新值,確保如果在事件發(fā)出后添加了監(jiān)聽器,監(jiān)聽器仍然可以立即訪問最新的事件 它允許在測試中通過覆蓋StreamProvider的方式來mock stream
ChangeNotifierProvider
ChangeNotifierProvider是一個用來管理Flutter中的ChangeNotifier的Provider。
Riverpod不鼓勵使用ChangeNotifierProvider,它的存在主要是為了下面這些場景。
從package:provider的代碼遷移到Riverpod時,替代原有的ChangeNotifierProvider 支持可變的狀態(tài)管理,但是,不可變的狀態(tài)是首選推薦的
?更傾向于使用StateNotifierProvider來代替。
只有當你絕對確定你想要可變的狀態(tài)時,才考慮使用ChangeNotifierProvider。
?
使用可變的狀態(tài)而不是不可變的狀態(tài)有時會更有效率。但缺點是,它可能更難維護,并可能破壞各種功能。
例如,如果你的狀態(tài)是可變的,使用provider.select來優(yōu)化Widget的重建可能就會失效,因為select會認為值沒有變化。
因此,使用不可變的數(shù)據(jù)結(jié)構(gòu)有時會更快。而且,針對你的用例進行基準測試很重要,以確保你通過使用ChangeNotifierProvider真正獲得了性能。
作為一個使用例子,我們可以使用ChangeNotifierProvider來實現(xiàn)一個todo-list。這樣做將允許我們公開諸如addTodo的方法,讓UI在用戶交互中修改todos列表。
class Todo {
Todo({
required this.id,
required this.description,
required this.completed,
});
String id;
String description;
bool completed;
}
class TodosNotifier extends ChangeNotifier {
final todos = <Todo>[];
// Let's allow the UI to add todos.
void addTodo(Todo todo) {
todos.add(todo);
notifyListeners();
}
// Let's allow removing todos
void removeTodo(String todoId) {
todos.remove(todos.firstWhere((element) => element.id == todoId));
notifyListeners();
}
// Let's mark a todo as completed
void toggle(String todoId) {
for (final todo in todos) {
if (todo.id == todoId) {
todo.completed = !todo.completed;
notifyListeners();
}
}
}
}
// Finally, we are using StateNotifierProvider to allow the UI to interact with
// our TodosNotifier class.
final todosProvider = ChangeNotifierProvider<TodosNotifier>((ref) {
return TodosNotifier();
});
現(xiàn)在我們已經(jīng)定義了一個ChangeNotifierProvider,我們可以用它來與用戶界面中的todos列表進行交互。
class TodoListView extends ConsumerWidget {
const TodoListView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
// rebuild the widget when the todo list changes
List<Todo> todos = ref.watch(todosProvider).todos;
// Let's render the todos in a scrollable list view
return ListView(
children: [
for (final todo in todos)
CheckboxListTile(
value: todo.completed,
// When tapping on the todo, change its completed status
onChanged: (value) =>
ref.read(todosProvider.notifier).toggle(todo.id),
title: Text(todo.description),
),
],
);
}
}
這些不同類型的各種Provider,就是我們的軍火庫,我們需要根據(jù)不同的場景和它們的特性來選擇不同的「武器」,通過文中給出的例子,相信大家能夠很好的理解它們的作用了。
向大家推薦下我的網(wǎng)站 https://xuyisheng.top/ 點擊原文一鍵直達
專注 Android-Kotlin-Flutter 歡迎大家訪問
往期推薦
更文不易,點個“三連”支持一下??
