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 ![plugin_provider_generator](https://hackmd.io/_uploads/rJWBJlH0ee.gif) :::info 由於生成範本未適配最新版 Riverpod,需手動將 `TestRef` 改為 `Ref` ::: ### NotifierProvider ![plugin_notifier_provider_generator](https://hackmd.io/_uploads/r1zUEgrAlx.gif) ### ConsumerStatefulWidget ![plugin_consumer_generator](https://hackmd.io/_uploads/S1fZ0lBAlg.gif) :::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); } } ``` :::