Riverpod 小部件刷新
===
從零開始
===
## 相關網站
- [Riverpod 官網](https://riverpod.dev/)
- [Riverpod Pub.dev](https://pub.dev/packages/riverpod)
## 安裝套件 (riverpod_generator)
在 terminal 輸入 (terminal 可以整段一次輸入)
```cmd
flutter pub add flutter_riverpod
flutter pub add riverpod_annotation
flutter pub add dev:riverpod_generator
flutter pub add dev:build_runner
flutter pub add dev:custom_lint
flutter pub add dev:riverpod_lint
```
## 新增 Riverpod 自定義分析器
在 analysis_options.yaml 新增
```yaml
analyzer:
plugins:
- custom_lint
```
## 新增 Riverpod Plugin
Android Studio -> Settings... -> Plugins -> Marketplace -> Flutter Riverpod Snippets -> Install
前置內容
===
## 代碼自動生成 (build_runner)
一次性生成,適用於修改部分邏輯,commit 前執行一次
```cmd
dart run build_runner build
```
持續生成,適用於日常開發 (`Command(⌘) + S` 可觸發生成,`Conrtol(⌃) + C` 可終止腳本)
```cmd
dart run build_runner watch
```
## Riverpod 作用範圍
```dart=
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
// Riverpod 作用範圍必須用 ProviderScope 包起來
runApp(ProviderScope(child: const MyApp()));
}
```
## 建立 Provider
使用 @riverpod 自動生成 Provider 需在 import 下方手動新增 part 'xxx.g.dart',xxx 必須與檔名相同,.g 檔案由 build_runner 自動生成 (請勿手動修改)
**test_func.dart**
```dart=
import 'package:riverpod_annotation/riverpod_annotation.dart';
// 需手動新增
part 'test_func.g.dart';
// 會自動生成 [testMessageProvider]
@riverpod
String testMessage(Ref ref) {
return 'Test Message';
}
```
## 使用 Provider
單純使用 Provider 不需新增 part
**test_widget.dart**
```dart=
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class TestMessageWidget extends ConsumerWidget {
const TestMessageWidget({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// 讀取 testMessageProvider 參數
final message = ref.read(testMessageProvider);
return Text(message);
}
}
```
## 善用套件生成範本
### Provider

:::info
由於生成範本未適配最新版 Riverpod,需手動將 `TestRef` 改為 `Ref`
:::
### NotifierProvider

### ConsumerStatefulWidget

:::info
IDE 補全 import 快捷為 `Option(⌥) + Enter(⏎)`
:::
主要內容
===
## 1. 讀取型別
回傳型別決定了 provider 能讀取到的型別
### 1-1. Provider
```dart=
/// [test1Provider] type is int
// 回傳 int 決定了 provider 能讀取到 int
@riverpod
int test1(Ref ref) => 1;
```
### 1-2. Notifier Provider
```dart=
/// [test2Provider] type is bool
@riverpod
class Test2 extends _$Test2 {
// 回傳 bool 決定了 provider 能讀取到 bool
@override
bool build() => false;
}
```
## 2. 讀取方式
### 2-1. Sync Provider
不需等待,可立即讀取參數
```dart=
/// [test3Provider] type is bool
@riverpod
bool test3(Ref ref) => false;
```
可直接讀取回傳型別
```dart=
void test3Func(Ref ref) {
final value = ref.read(test3Provider);
// value type is bool
if (value) {...}
}
```
### 2-2. Async Provider
需要等待,無法立即讀取參數
```dart=
/// [test4Provider] type is AsyncValue<bool>
@riverpod
FutureOr<bool> test4(Ref ref) => false;
/// [test5Provider] type is AsyncValue<bool>
@riverpod
Future<bool> test5(Ref ref) async => false;
/// [test6Provider] type is AsyncValue<bool>
@riverpod
Stream<bool> test6(Ref ref) async* { yield false; }
/// [test7Provider] type is AsyncValue<bool>
@riverpod
AsyncValue<bool> test7(Ref ref) => const AsyncData(false);
```
以上全部都會讀取到 `AsyncValue<回傳型別>`
```dart=
void test4Func(Ref ref) {
final value = ref.read(test4Provider);
// value type is AsyncValue<bool>
❌ if (value) {...} // AsyncValue 使用方法後續會提及
}
```
也可使用 `.future` 等待讀取回傳型別
```dart=
Future<void> test5Func(Ref ref) async {
final value = await ref.read(test4Provider.future);
// value type is bool
if (value) {...}
}
```
### 2-3. AsyncValue
`AsyncValue` 被 `AsyncData`、`AsyncError`、`AsyncLoading` 共 3 種狀態繼承,以 `Future` 來說,讀取瞬間若 `Future` 尚未完成會觸發 `AsyncLoading`,讀取瞬間若 `Future` 已完成會觸發 `AsyncData`,且 `AsyncData` 會帶有原始回傳型別
:::info
以下會同時列出 ==過時寫法== 以及 ==主流寫法==,大部分舊專案都用 ==過時寫法==,至少要看得懂,Dart 3.0 後建議優先用 ==主流寫法==
:::
#### 2-3-1. 過時寫法 (.when)
- 優點:IDE 可幫忙生成範本,且支援 `.whenData`、`skipLoadingOnReload`、`skipLoadingOnRefresh`、`skipError` 等特殊用法
`.when` 需在每一種狀態決定回傳參數
```dart=
void test6Func(Ref ref) {
final value = ref.read(test4Provider);
// value type is AsyncValue<bool>
// .when 處理所有狀態
final newValue = value.when(
data: (data) => data, // data type is bool
error: (error, stackTrace) => false,
loading: () => false,
);
// newValue type is bool
}
```
`.when` 可以任意轉換回傳型別,也有 `.whenOrNull`、`.maybeWhen` 各種延伸使用方法
```dart=
void test7Func(Ref ref) {
final value = ref.read(test4Provider);
// value type is AsyncValue<bool>
// 所有狀態都回傳 String
final newValue1 = value.when(
data: (data) => '$data', // data type bool -> String
error: (error, stackTrace) => 'error',
loading: () => 'loading',
);
// newValue1 type is String
// 不同狀態回傳 2 種以上型別
final newValue2 = value.when(
data: (data) => '$data',
error: (error, stackTrace) => error,
loading: () => 123,
);
// newValue2 type is Object
// 只處理指定狀態, 其餘狀態回傳 null
final newValue3 = value.whenOrNull(
data: (data) => '$data',
loading: () => 'loading',
);
// newValue3 type is String?
// 只處理指定狀態, 其餘狀態執行 orElse
final newValue4 = value.maybeWhen(
data: (data) => 111,
error: (error, stackTrace) => 222,
orElse: () => 333,
);
// newValue4 type is int
// data, loading 做相同處理, 針對 error 做不同處理
final newValue5 = value.when(
data: (data) => 'data or loading',
error: (error, stackTrace) {
if (value.hasValue) {
return 'error value: ${value.requireValue}';
} else if (error is StateError) {
return 'StateError message: ${error.message}';
} else {
return 'others error: $error';
}
},
loading: () => 'data or loading',
);
// newValue5 type is String
}
```
#### 2-3-2. 主流寫法 (switch-case)
- 優點:官方推薦,強大的 dart switch-case 功能
基本用法
```dart=
void test8Func(Ref ref) {
final value = ref.read(test4Provider);
// value type is AsyncValue<bool>
final newValue = switch (value) {
AsyncData(:final value) => value,
AsyncError() => false,
AsyncLoading() => false,
};
// newValue type is bool
}
```
用 switch-case 處理上述過時寫法
```dart=
void test9Func(Ref ref) {
final value = ref.read(test4Provider);
// value type is AsyncValue<bool>
// 所有狀態都回傳 String
final newValue1 = switch (value) {
AsyncData(:final value) => '$value', // data type bool -> String
AsyncError() => 'error',
AsyncLoading() => 'loading',
};
// newValue1 type is String
// 不同狀態回傳 2 種以上型別
final newValue2 = switch (value) {
AsyncData(:final value) => '$value',
AsyncError(:final error) => error,
AsyncLoading() => 123,
};
// newValue2 type is Object
// 只處理指定狀態, 其餘狀態回傳 null
final newValue3 = switch (value) {
AsyncData(:final value) => '$value',
AsyncLoading() => 'loading',
_ => null,
};
// newValue3 type is String?
// 只處理指定狀態, 其餘狀態執行 default
final newValue4 = switch (value) {
AsyncData() => 111,
AsyncLoading() => 222,
_ => 333,
};
// newValue4 type is int
// data, loading 做相同處理, 針對 error 做不同處理
final newValue5 = switch (value) {
AsyncData() || AsyncLoading() => 'data or loading',
AsyncError() when value.hasValue =>
'error value: ${value.requireValue}',
AsyncError(:final StateError error) =>
'StateError message: ${error.message}',
AsyncError(:final error) => 'others error: $error',
};
// newValue5 type is String
}
```
## 3. 帶參數的 Provider (Family)
讀取時 provider 後方要加 (),且可以帶入相關參數
### 3-1. Provider
```dart=
/// [test10Provider] type is String
/// 參數定義在 Ref ref 後方
@riverpod
String test10(Ref ref, int value) => 'string from int: $value';
void test10Fun(Ref ref) {
final result = ref.read(test10Provider(10));
// result: string from int: 10
}
```
:::warning
Family Provider 可不帶入參數時, 仍必須要加 ()
```dart=
/// [test11Provider] type is String
@riverpod
String test11(Ref ref, {int value = 3}) => 'value is: $value';
void test11Fun(Ref ref) {
❌ final result = ref.read(test11Provider);
⭕️ final result = ref.read(test11Provider());
// result: value is: 3
}
```
:::
### 3-2. Notifier Provider
```dart=
/// [test12Provider] type is String
@riverpod
class Test12 extends _$Test12 {
// 參數定義在 build() 之中
@override
String build({required int value1, required int value2}) =>
'$value1 + $value2 = ${value1 + value2}';
}
void test12Fun(Ref ref) {
final result = ref.read(test12Provider(value1: 1, value2: 2));
// result: 1 + 2 = 3
}
```
## 4. Provider vs Notifier Provider
一般 Provider 與 Notifier Provider 最大的差異是 Notifier Provider 可以後期控制自身的參數
### 4-1. Provider
回傳參數後,就無法再發生改變
#### 4-1-1. Sync
```dart=
/// [test13Provider] type is int
@riverpod
int test13(Ref ref) => 1;
```
#### 4-1-2. Async
```dart=
/// [test14Provider] type is AsyncValue<int>
@riverpod
Future<int> test14(Ref ref) async => 1;
```
### 4-2. Notifier Provider
可透過 `state` 變更參數,`state` 參數型別與 return 型別一致
#### 4-2-1. Sync
```dart=
/// [test15Provider] type is int
@riverpod
class Test15 extends _$Test15 {
@override
int build() => 1;
void addOne(int value) {
// state type is int
state = value + 1;
}
}
```
#### 4-2-2. Async
```dart=
/// [test16Provider] type is AsyncValue<int>
@riverpod
class Test16 extends _$Test16 {
@override
Future<int> build() async {
await Future.delayed(const Duration(seconds: 1));
return 1;
}
Future<void> addTwo(int value) async {
// state type is AsyncValue<int>
state = const AsyncLoading();
await Future.delayed(const Duration(seconds: 1));
state = AsyncData(value + 2);
}
}
```
#### 4-2-3. Call Notifier Provder Function
只有 Notifier Provider 有 `.notifier` 方法,讀取 `.notifier` 可拿到 Provider 原始物件,即可透過物件呼叫自定義方法
```dart=
Future<void> test15Func(Ref ref) async {
final test15 = ref.read(test15Provider.notifier);
// test15 type is Test15
test15.addOne(2);
await ref.read(test16Provider.notifier).addTwo(3);
}
```
## 5. read/listen/watch
Provider 參數是可以持續變化的,依據功能需求功能也可以用不同方式讀取
```dart=
/// [test17Provider] type is AsyncValue<int>
@riverpod
Stream<int> test17(Ref ref) async* {
await Future.delayed(const Duration(seconds: 1));
yield 1;
await Future.delayed(const Duration(seconds: 1));
yield 2;
await Future.delayed(const Duration(seconds: 1));
yield 3;
}
// 實際執行:
// AsyncLoading<int>()
// 等 1 秒
// AsyncData<int>(1)
// 等 1 秒
// AsyncData<int>(2)
// 等 1 秒
// AsyncData<int>(3)
```
### 5-1. read
只讀取一次當前參數,後續不再更新
```dart=
void test17Func(Ref ref) {
final result = ref.read(test17Provider);
print('result: $result');
}
// 執行結果:
// result: AsyncLoading<int>()
```
:::warning
由於 read 瞬間,test17Provider 剛被創立,故只會讀到 AsyncLoading 參數
:::
### 5-2. listen
持續監聽參數變化,不刷新 ref 自身 (後續會提及)
```dart=
void test18Func(Ref ref) {
print('start listen');
ref.listen(test17Provider, (previous, next) {
print('previous: $previous, next: $next');
});
}
// 執行結果:
// start listen
// 過 1 秒
// previous: AsyncLoading<int>(), next: AsyncData<int>(value: 1)
// 過 1 秒
// previous: AsyncData<int>(value: 1), next: AsyncData<int>(value: 2)
// 過 1 秒
// previous: AsyncData<int>(value: 2), next: AsyncData<int>(value: 3)
```
:::warning
由於 listen 瞬間,test17Provider 剛被創立,AsyncLoading 為初始值,並非變化,需等到 1 秒後的事件,才會聽到變化
:::
### 5-3. listen (with fireImmediately)
持續監聽參數變化,不刷新 ref 自身 (後續會提及),且立即回傳當前參數
```dart=
void test19Func(Ref ref) {
print('start listen');
ref.listen(test17Provider, (previous, next) {
print('previous: $previous, next: $next');
}, fireImmediately: true);
}
// 執行結果:
// start listen
// previous: null, next: AsyncLoading<int>()
// 過 1 秒
// previous: AsyncLoading<int>(), next: AsyncData<int>(value: 1)
// 過 1 秒
// previous: AsyncData<int>(value: 1), next: AsyncData<int>(value: 2)
// 過 1 秒
// previous: AsyncData<int>(value: 2), next: AsyncData<int>(value: 3)
```
:::info
fireImmediately 相當於立即 read 一次,並透過 listen 回傳結果
:::
### 5-4. watch
持續監聽參數變化,且刷新 ref 自身 (後續會提及)
```dart=
void test20Func(Ref ref) {
final result = ref.watch(test17Provider);
print('result: $result');
}
// 執行結果:
// result: AsyncLoading<int>()
// 過 1 秒
// result: AsyncData<int>(1)
// 過 1 秒
// result: AsyncData<int>(2)
// 過 1 秒
// result: AsyncData<int>(3)
```
## 6. Ref 主要來源與刷新作用域
`ref.read` 與 `ref.listen` 當參數發生變化時,皆不會觸發 ref 刷新,只有 `ref.watch` 當參數發生變化時,會觸發 ref 刷新,刷新範圍僅提供 ref 本身的物件
### 6-1. Widget (WidgetRef)
`ConsumerWidget`、`ConsumerStatefulWidget`、`Consumer` 是三種最常見的 Widget,拿到的 ref 都屬於 `WidgetRef`
#### 6-1-1. ConsumerWidget
只能透過 `Widget build(BuildContext context, WidgetRef ref)` 方法拿到 ref,當 `ref.watch` 參數變化,會觸發 `build` 刷新
```dart=
class Test21Widget extends ConsumerWidget {
const Test21Widget({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// 只有這裡可拿到 ref, 且 watch 觸發 build 刷新
final result = ref.watch(someProvider);
return testWidget(ref);
}
Widget testWidget(WidgetRef ref) {
// 必須倚靠上層帶入, 才能拿到 ref, 且 watch 觸發 build 刷新
final result = ref.watch(someProvider);
return Container();
}
}
```
#### 6-1-2. ConsumerStatefulWidget
`ConsumerState` 有定義 WidgetRef ref,`ConsumerState` 底下都可拿到屬於 Widget 自身的 ref,當 `ref.watch` 參數變化,會觸發 `build` 刷新
```dart=
class Test22Widget extends ConsumerStatefulWidget {
const Test22Widget({super.key});
@override
ConsumerState createState() => _Test22WidgetState();
}
class _Test22WidgetState extends ConsumerState<Test22Widget> {
@override
Widget build(BuildContext context) {
// 可拿到 ref, 且 watch 觸發 build 刷新
final result = ref.watch(someProvider);
return Container();
}
void test() {
// 可拿到 ref, 且 watch 觸發 build 刷新
final result = ref.watch(someProvider);
}
}
```
#### 6-1-3. Consumer
只能透過 `builder` 方法拿到 ref,當 `ref.watch` 參數變化,會觸發 `builder` 刷新
```dart=
Consumer(builder: (context, ref, child) {
// 只有這裡可拿到 ref, 且 watch 觸發 builder 刷新
final result = ref.watch(someProvider);
return Container();
});
```
:::info
Consumer 通常會被包在其他 Widget 底下,Consumer 的 ref 與 Parent Widget 的 ref 不共用,僅刷新 Consumer 自身的 `builder` 方法,不會觸發 Parent Widget 的 `build` 方法
:::
### 6-2. Provider (Ref)
`Provider`、`Notifier Provider` 拿到的 ref 都屬於 `Ref`
#### 6-2-1. Provider
只能透過方法本身拿到 ref,當 `ref.watch` 參數變化,`方法` 會被重新呼叫
```dart=
@riverpod
bool test23(Ref ref) {
// 只有這裡可拿到 ref, 且 watch 觸發 test23 方法重新呼叫
final result = ref.watch(otherProvider);
return false;
}
```
#### 6-2-2. Notifier Provider
被繼承的 `_$XXX` 有定義 Ref ref,`_$XXX` 底下都可拿到屬於 Notifier 自身的 ref,當 `ref.watch` 參數變化,會觸發 `build` 刷新
```dart=
@riverpod
class Test24 extends _$Test24 {
@override
bool build() {
// 可拿到 ref, 且 watch 觸發 build 刷新
final result = ref.watch(otherProvider);
return false;
}
void test() {
// 可拿到 ref, 且觸發 build 刷新, 且 watch 觸發 build 刷新
final result = ref.watch(otherProvider);
}
}
```
## 7. 生命週期
Provider 會根據 read/listen/watch 或是被 Widget/Provider 讀取,會有不同的生命週期,當 Provider 沒人使用,則會自動銷毀
### 7-1. read
`ref.read` 時,Provider 只會存在一瞬間
- Provider 沒他人使用:Provider 建立 -> read 回傳參數 -> Provider 銷毀
```dart=
Future<void> test25Func(Ref ref) async {
print('first read');
ref.read(someProvider);
print('first read complete');
await Future.delayed(const Duration(seconds: 1));
print('second read');
ref.read(someProvider);
print('second read complete');
}
// 執行結果:
// first read
// someProvider create
// first read complete
// someProvider dispose
// 過 1 秒
// second read
// someProvider create
// second read complete
// someProvider dispose
```
- Provider 有他人使用:read 回傳當前 Provider 參數
```dart=
Future<void> test26Func(Ref ref) async {
ref.watch(someProvider); // 持續監聽
print('first read');
ref.read(someProvider);
print('first read complete');
await Future.delayed(const Duration(seconds: 1));
print('second read');
ref.read(someProvider);
print('second read complete');
}
// 執行結果:
// someProvider create
// first read
// first read complete
// 過 1 秒
// second read
// second read complete
```
### 7-2. WidgetRef listen/watch
`ref.listen`、`ref.watch` 生命週期與 Widget 本身同步,Widget 銷毀,Provider 則同步銷毀
```dart=
Consumer(builder: (context, ref, child) {
// Consumer 銷毀, someProvider 沒他人使用也會自動銷毀
final result = ref.watch(someProvider);
return Container();
});
```
### 7-3. Ref listen/watch
`ref.listen`、`ref.watch` 生命週期與 Provider 同步,當前 Provider 銷毀,另一個 Provider 則同步銷毀
```dart=
@riverpod
bool test27(Ref ref) {
// test27Provider 銷毀, otherProvider 沒他人使用也會自動銷毀
final result = ref.watch(otherProvider);
return false;
}
```
### 7-4. KeepAlive
大部需求都允許 Provider 沒人使用時自動銷毀,少部分需求不希望被銷毀,則需要 KeepAlive 功能
#### 7-4-1. 永久 KeepAlive
`@riverpod` 改為 `@Riverpod(keepAlive: true)`,在第一次讀取時建立,再來永久保持 Provider 不被銷毀
- 適用場景:Singleton
```dart=
@Riverpod(keepAlive: true)
bool test28(Ref ref) => false;
Future<void> test28Func(Ref ref) async {
print('first read');
ref.read(test28Provider);
print('first read complete');
await Future.delayed(const Duration(seconds: 1));
print('second read');
ref.read(test28Provider);
print('second read complete');
}
// 執行結果:
// first read
// test28Provider create
// first read complete
// 過 1 秒
// second read
// second read complete
```
#### 7-4-2. 暫時 KeepAlive
使用 `ref.keepAlive` 可拿到 `KeepAliveLink` 物件,直到對 `KeepAliveLink` 呼叫 `close()`,Provider 才會被銷毀
- 適用場景:執行耗時任務
```dart=
@riverpod
class Test29 extends _$Test29 {
@override
bool build() => false;
Future<void> upload() async {
final link = ref.keepAlive();
state = true;
await Future.delayed(const Duration(seconds: 2));
state = false;
link.close();
}
}
Future<void> test29Func(Ref ref) async {
print('first read');
ref.read(test29Provider.notifier).upload();
print('first read complete');
await Future.delayed(const Duration(seconds: 1));
print('second read');
ref.read(test29Provider);
print('second read complete');
}
// 執行結果:
// first read
// test29Provider create
// first read complete
// 過 1 秒
// second read
// second read complete
// 過 1 秒
// test29Provider dispose
```
## 8. 唯一性
Provider 是唯一的,在 Provider 沒有被銷毀前,所有人透過 Provider 拿到的參數都是同一個
### 8-1. Provider
```dart=
@Riverpod(keepAlive: true)
int test30(Ref ref) => Random().nextInt(100);
void test30Func(Ref ref) {
final result1 = test30Provider == test30Provider;
print('result1: $result1');
final result2 = ref.read(test30Provider);
print('result2: $result2');
final result3 = ref.read(test30Provider);
print('result3: $result3');
}
// 執行結果:
// result1: true
// result2: 74
// result3: 74
```
### 8-2. Family Provider
Family Provider 會比對每一個帶入的參數,當每個參數 `==` 皆相同,所有人透過 Provider 拿到的參數才會是同一個
:::warning
請注意 class/List/Map 本身都不符合 `==`,通常 class 都會透過 @freezed 實作 `==` 部分
:::
```dart=
@Riverpod(keepAlive: true)
int test31(Ref ref, {required int value}) => Random().nextInt(100);
void test31Func(Ref ref) {
final result1 = test31Provider(value: 1) == test31Provider(value: 1);
print('result1: $result1');
final result2 = test31Provider(value: 1) == test31Provider(value: 2);
print('result2: $result2');
final result3 = ref.read(test31Provider(value: 1));
print('result3: $result3');
final result4 = ref.read(test31Provider(value: 1));
print('result4: $result4');
final result5 = ref.read(test31Provider(value: 2));
print('result5: $result5');
}
// 執行結果:
// result1: true
// result2: false
// result3: 15
// result4: 15
// result5: 19
```
## 9. 狀態過濾
Provider 當參數發生變化時,會對上一個狀態進行 `==` 比對,參數不同才會更新狀態
:::warning
請注意 class/List/Map 本身都不符合 `==`,通常 class 都會透過 @freezed 實作 `==` 部分
:::
### 9-1. 基本過濾
```dart=
@riverpod
class Test32 extends _$Test32 {
@override
int build() {
Future(() async {
await Future.delayed(const Duration(seconds: 1));
state = 1;
await Future.delayed(const Duration(seconds: 1));
// 上一個狀態還是 1, 會被忽略
state = 1;
await Future.delayed(const Duration(seconds: 1));
state = 2;
await Future.delayed(const Duration(seconds: 1));
state = 1;
});
return 0;
}
}
void test32Func(Ref ref) {
final result = ref.watch(test32Provider);
print('result: $result');
}
// 執行結果:
// result: 0
// 過 1 秒
// result: 1
// 過 2 秒
// result: 2
// 過 1 秒
// result: 1
```
### 9-2. 運算後過濾
`provider.select` 可以先拿到每一次變化,經由運算或是型別轉換後再進行一次比對,比對後參數不同才進行刷新
```dart=
@riverpod
class Test33 extends _$Test33 {
@override
int build() {
Future(() async {
while (true) {
await Future.delayed(const Duration(seconds: 1));
state++;
}
});
return 0;
}
}
void test33Func(Ref ref) {
final result = ref.watch(
test33Provider.select((value) {
// 0, 1, 2 都為 false, 3 之後變為 true, 觸發刷新
return value >= 3;
}),
);
print('result: $result');
}
// 執行結果:
// result: false
// 過 3 秒
// result: true
```
### 9-3. select 使用時機
#### 9-3-1. 運算單純
Provider 若是運算單純,不一定要用 `provider.select` 功能,因為 Provider 的回傳本身就會再比對一次
```dart=
// 假設有 3 個參數為 Point(1, 3) -> Point(2, 2) -> Point(3, 1)
@riverpod
Point<int> test34(Ref ref) {...}
@riverpod
int test35(Ref ref) {
// watch 內容刷新 3 次, 內容 3 個參數皆不同, test35 觸發 3 次刷新
final point = ref.watch(test34Provider);
// x + y 共運算 3 次
return point.x + point.y;
}
// test35Provider 內容刷新 3 次, 共比對 3 次, 參數皆為 4
// ref.watch(test35Provider) 最終只觸發 1 次刷新
@riverpod
int test36(Ref ref) {
// select x + y 共運算 3 次
// watch 內容刷新 3 次, 共比對 3 次, 參數皆為 4, test36 只觸發 1 次刷新
return ref.watch(test34Provider.select((point) => point.x + point.y));
}
// test36Provider 內容刷新 1 次, 共比對 1 次, 參數為 4
// ref.watch(test36Provider) 最終只觸發 1 次刷新
```
:::info
兩者並沒有減少運算次數,故使用 test35 相對簡單,雖然 test35Provider 會經歷較多次 update,但最終效能差不多
:::
#### 9-3-2. 過濾不必要參數
只需用到一個參數的一部分時,可以用 `provider.select` 過濾部分參數
```dart=
// 假設有 3 個參數為 Point(1, 2) -> Point(2, 2) -> Point(3, 2)
@riverpod
Point<int> test37(Ref ref) {...}
@riverpod
int test38(Ref ref) {
// watch 內容刷新 3 次, 內容 3 個參數皆不同, test38 觸發 3 次刷新
final point = ref.watch(test37Provider);
// y + 2 共運算 3 次
return point.y + 2;
}
// test38Provider 內容刷新 3 次, 共比對 3 次, 參數皆為 4
// ref.watch(test38Provider) 最終只觸發 1 次刷新
@riverpod
int test39(Ref ref) {
// watch 內容刷新 3 次, 共比對 3 次, 參數皆為 2, test36 只觸發 1 次刷新
final y = ref.watch(test37Provider.select((point) => point.y));
// y + 2 共運算 1 次
return y + 2;
}
// test39Provider 內容刷新 1 次, 共比對 1 次, 參數為 4
// ref.watch(test39Provider) 最終只觸發 1 次刷新
```
:::info
由於運算只需要 y,故僅監聽 y 可以避免 x 變化而觸發沒必要的刷新,有效降低運算次數
:::
#### 9-3-3. 運算複雜
可利用 `provider.select` 進行簡單運算後過濾,以減少後續複雜運算次數
```dart=
@riverpod
Point<int> test40(Ref ref) {...}
// 假設半徑是簡單運算
int calRadius(Point<int> point) {...}
// 假設面積是複雜運算
int calArea(int radius) {...}
@riverpod
int test41(Ref ref) {
// 每次點發生變化, 都要經過簡單半徑以及複雜面積運算
final point = ref.watch(test40Provider);
final radius = calRadius(point);
return calArea(radius);
}
@riverpod
int test42(Ref ref) {
// 每次點發生變化, 先過濾不同的半徑
final radius = ref.watch(
test40Provider.select((point) => calRadius(point))
);
// 當半徑有變化, 再進行複雜面積運算
return calArea(radius);
}
```
:::info
若 `point` 剛好點在相同半徑上移動,test42 只要進行一次複雜運算
:::
## 10. 組合監聽
Provider 可以監聽多個 Provider,遵守 `ref.read`、`ref.listen` 不會觸發刷新,只有 `ref.watch` 會觸發刷新
:::warning
Provider 之間不能循環監聽,例如 A watch B,B 又 watch A
:::
```dart=
@riverpod
int test43(Ref ref) => 1;
@riverpod
bool test44(Ref ref) => false;
@riverpod
String test45(Ref ref) => 'hello';
@riverpod
String test46(Ref ref) {
// 若 test43Provider 發生變化, 不會觸發 test46Provider 刷新
final value1 = ref.read(test43Provider);
// 若 test44Provider 發生變化, 會觸發 test46Provider 刷新
final value2 = ref.watch(test44Provider);
// 若 test45Provider 發生變化, 會觸發 test46Provider 刷新
final value3 = ref.watch(test45Provider);
return 'value1: $value1, value2: $value2, value3: $value3';
}
void test46Func(Ref ref) {
final result = ref.watch(test46Provider);
print('result: $result');
}
// 執行結果:
// result: value1: 1, value2: false, value3: hello
```
高效刷新範例
===
## 範例 1
```dart=
@freezed
abstract class Message with _$Message {
const factory Message(String content, double progress) = _Message;
}
// 6 筆資料
Stream<Message> get messageStream => Stream.fromIterable([
Message('first', 1),
Message('first', 1),
Message('first', 2),
Message('first', 2),
Message('second', 2),
Message('second', 2),
]);
```
:::danger
**BAD**
每次狀態更新,就算 Message 內容完全相同,都會刷新整個元件
```dart=
class MessageProgress extends StatefulWidget {
const MessageProgress({super.key});
@override
State<MessageProgress> createState() => _MessageProgressState();
}
class _MessageProgressState extends State<MessageProgress> {
Message? _message;
@override
void initState() {
super.initState();
// 於 initState 開始監聽 messageStream
messageStream.listen((event) {
setState(() {
// 每次都觸發 build 刷新, 收到 6 次更新, 刷新 6 次
_message = event;
});
});
}
@override
Widget build(BuildContext context) {
// Column 創建 7 次 (含 _message 未賦值 1 次)
return Column(
children: [
// Text 創建 7 次
Text('Message'),
// Text 創建 7 次
Text(_message?.content ?? ''),
// LinearProgressIndicator 創建 7 次
LinearProgressIndicator(value:_message?.progress ?? 0),
],
);
}
}
```
:::
:::danger
**BAD**
ref 為大範圍 ref,刷新範圍太大
```dart=
// AsyncLoading ->
// AsyncData(Message('first', 1)) ->
// AsyncData(Message('first', 2)) ->
// AsyncData(Message('second', 2))
@riverpod
Stream<Message> message(Ref ref) => messageStream;
class MessageProgress extends ConsumerWidget {
const MessageProgress({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// ref 為此 MessageProgress 的 ref, 狀態變化會觸發此 build 刷新
final message = ref.watch(messageProvider).value;
// Column 創建 4 次
return Column(
children: [
// Text 創建 4 次
Text('Message'),
// Text 創建 4 次
Text(message?.content ?? ''),
// LinearProgressIndicator 創建 4 次
LinearProgressIndicator(value: message?.progress ?? 0),
],
);
}
}
```
:::
:::success
**GOOD**
ref 為小範圍 ref,只有相關資料有變化才會刷新相關元件
```dart=
// AsyncLoading ->
// AsyncData(Message('first', 1)) ->
// AsyncData(Message('first', 2)) ->
// AsyncData(Message('second', 2))
@riverpod
Stream<Message> message(Ref ref) => messageStream;
// '' -> 'first' -> 'second'
@riverpod
String messageContent(Ref ref) =>
ref.watch(messageProvider).value?.content ?? '';
// 0 -> 1 -> 2
@riverpod
double messageProgress(Ref ref) =>
ref.watch(messageProvider).value?.progress ?? 0;
class MessageProgress extends StatelessWidget {
const MessageProgress({super.key});
@override
Widget build(BuildContext context) {
// Column 創建 1 次
return Column(
children: [
// Text 創建 1 次
Text('Message'),
Consumer(
builder: (context, ref, child) {
// ref 為此 Consumer 的 ref, 狀態變化會觸發此 builder 刷新
// Text 創建 3 次
final content = ref.watch(messageContentProvider);
return Text(content);
},
),
Consumer(
builder: (context, ref, child) {
// ref 為此 Consumer 的 ref, 狀態變化會觸發此 builder 刷新
// LinearProgressIndicator 創建 3 次
final progress = ref.watch(messageProgressProvider);
return LinearProgressIndicator(value: progress);
},
),
],
);
}
}
```
:::
## 範例 2
```dart=
Stream<bool> get isConnectStream =>
Stream.fromIterable([false, true, false]);
Stream<int> get randomIntStream async* {
while (true) {
await Future.delayed(const Duration(seconds: 1));
yield Random().nextInt(100);
}
}
```
:::danger
**BAD**
每次任何一個狀態更新,都會刷新整個元件
```dart=
class ConnectInt extends StatefulWidget {
const ConnectInt({super.key});
@override
State<ConnectInt> createState() => _ConnectIntState();
}
class _ConnectIntState extends State<ConnectInt> {
bool? _isConnect;
int? _randomInt;
@override
void initState() {
super.initState();
// 於 initState 開始監聽 isConnectStream, randomIntStream
isConnectStream.listen((event) {
setState(() {
// 每次都觸發 build 刷新
_isConnect = event;
});
});
randomIntStream.listen((event) {
setState(() {
// 每次都觸發 build 刷新
_randomInt = event;
});
});
}
@override
Widget build(BuildContext context) {
if (_isConnect ?? false) {
return Text('connect int: ${_randomInt ?? -1}');
} else {
// 未連線不使用 randomInt, 但 randomInt 還是會持續被刷新
return Text('not connect');
}
}
}
```
:::
:::success
**GOOD**
Provider 只在必要的時候監聽
```dart=
@riverpod
Stream<bool> isConnect(Ref ref) => isConnectStream;
@riverpod
Stream<int> randomInt(Ref ref) => randomIntStream;
@riverpod
String connectIntMessage(Ref ref) {
final isConnect = ref.watch(isConnectProvider);
if (isConnect.value ?? false) {
final randomInt = ref.watch(randomIntProvider);
return 'connect int: ${randomInt.value ?? -1}';
} else {
// 未連線連不使用 randomIntProvider, provider 不會被創建
return 'not connect';
}
}
class ConnectInt extends ConsumerWidget {
const ConnectInt({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final message = ref.watch(connectIntMessageProvider);
return Text(message);
}
}
```
:::