一、riverpod狀態管理中所涉及到的widget UI組件對比分析
UI 組件 | 狀態類型 | 語法形式 | 特點 |
---|---|---|---|
ConsumerWidget | 有狀態 | 無狀態形式 | 最常用,通過WidgetRef訪問provider, 所謂無狀態,是指ConsumerWidegt不像StatefulWidegt那樣創建state,在它內部不可以定義狀態變量,然后再調用setState()更新狀態和UI,類似于statelessWidget,但是可以在它內部引用外部的或全局狀態提供者provider,以達到全局狀態提供者狀態更新時,ConsumerWidget也重新構建UI |
ConsumerStatefulWidget | 有狀態 | 有狀態形式 | 具有完整生命周期,可管理內部狀態, 類似于StatefulWidget, 創建狀態,重載createState() 初始化狀態,重截initState(), 狀態銷毀,重載dispose() |
Consumer | 有狀態 | ?--- | 局部UI重建,只重建部分UI,優化性能 |
ProviderScope | 有狀態 | ?--- | 創建新的provider作用域,可覆蓋父級provider |
HookWidget | 有狀態 | 無狀態形式 | 使用 Hooks(鉤子),依賴flutter_hooks這個庫,使用useState 在無狀態Widget中管理狀態和其他副作用,生命周期使用useEffect |
HookConsumerWidget | 有狀態 | 無狀態形式 | 可以同時使用 Hooks + Riverpod管理狀態,生命周期使用useEffect |
下面用代碼分析比較一下使用場景:
1. ConsumerWidget - 最常用的UI組件
final counterProvider = StateProvider<int>((ref) => 0);class ConsumerWidgetExample extends ConsumerWidget {const ConsumerWidgetExample({super.key});@overrideWidget build(BuildContext context, WidgetRef ref) {final counter = ref.watch(counterProvider);return Card(child: Padding(padding: const EdgeInsets.all(16.0),child: Column(crossAxisAlignment: CrossAxisAlignment.start,children: [const Text('1. ConsumerWidget',style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),),const SizedBox(height: 10),Text('這是一個無狀態組件,通過WidgetRef訪問provider'),const SizedBox(height: 10),Text('計數器: $counter', style: const TextStyle(fontSize: 20)),const SizedBox(height: 10),ElevatedButton(onPressed: () => ref.read(counterProvider.notifier).state++,child: const Text('增加計數'),),],),),);}
}
2. ConsumerStatefulWidget - 需要內部狀態的UI組件
final counterProvider = StateProvider<int>((ref) => 0);class ConsumerStatefulWidgetExample extends ConsumerStatefulWidget {const ConsumerStatefulWidgetExample({super.key});@overrideConsumerState<ConsumerStatefulWidgetExample> createState() => _ConsumerStatefulWidgetExampleState();
}class _ConsumerStatefulWidgetExampleState extends ConsumerState<ConsumerStatefulWidgetExample> {int _localClicks = 0;@overridevoid initState() {super.initState();debugPrint('ConsumerStatefulWidget 初始化');}@overridevoid dispose() {debugPrint('ConsumerStatefulWidget 被銷毀');super.dispose();}@overrideWidget build(BuildContext context) {final counter = ref.watch(counterProvider);return Card(child: Padding(padding: const EdgeInsets.all(16.0),child: Column(crossAxisAlignment: CrossAxisAlignment.start,children: [const Text('2. ConsumerStatefulWidget',style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),),const SizedBox(height: 10),Text('這是一個有狀態組件,可以管理內部狀態'),const SizedBox(height: 10),Text('全局計數器: $counter', style: const TextStyle(fontSize: 16)),Text('本地點擊次數: $_localClicks', style: const TextStyle(fontSize: 16)),const SizedBox(height: 10),Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly,children: [ElevatedButton(onPressed: () {ref.read(counterProvider.notifier).state++;},child: const Text('全局+1'),),ElevatedButton(onPressed: () {setState(() {_localClicks++;});},child: const Text('本地+1'),),],),],),),);}
}
3. Consumer - 用于局部UI重建
final counterProvider = StateProvider<int>((ref) => 0);class ConsumerExample extends ConsumerWidget {const ConsumerExample({super.key});@overrideWidget build(BuildContext context, WidgetRef ref) {return Card(child: Padding(padding: const EdgeInsets.all(16.0),child: Column(crossAxisAlignment: CrossAxisAlignment.start,children: [const Text('3. Consumer',style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),),const SizedBox(height: 10),const Text('使用Consumer只重建UI的特定部分:'),const SizedBox(height: 10),// 這個Text不會在計數器變化時重建const Text('這是靜態文本,不會重建'),const SizedBox(height: 10),// 只有Consumer內的部分會在計數器變化時重建Consumer(builder: (context, ref, child) {final counter = ref.watch(counterProvider);return Text('動態計數: $counter',style: const TextStyle(fontSize: 20, color: Colors.blue),);},),const SizedBox(height: 10),ElevatedButton(onPressed: () => ref.read(counterProvider.notifier).state++,child: const Text('增加計數'),),],),),);}
}
4. ProviderScope - 用于創建新的provider作用域
????ProviderScope示例1
final counterProvider = StateProvider<int>((ref) => 0);class ProviderScopeExample extends StatelessWidget {const ProviderScopeExample({super.key});@overrideWidget build(BuildContext context) {// 創建一個新的provider作用域,可以覆蓋父級的providerreturn ProviderScope(child: Card(child: Padding(padding: const EdgeInsets.all(16.0),child: Column(crossAxisAlignment: CrossAxisAlignment.start,children: [const Text('4. ProviderScope',style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),),const SizedBox(height: 10),const Text('創建一個新的provider作用域'),const SizedBox(height: 10),// 在這個作用域內,可以覆蓋父級的providerConsumer(builder: (context, ref, child) {final counter = ref.watch(counterProvider);return Text('計數器: $counter');},),],),),),);}
}
ProviderScope示例2:當我們有一個ListView
顯示產品列表,每個項目都需要知道正確的產品ID或索引時:
class ProductItem extends StatelessWidget {const ProductItem({super.key, required this.index});final int index;@overrideWidget build(BuildContext context) {// do something with the index}}class ProductList extends StatelessWidget {@overrideWidget build(BuildContext context) {return ListView.builder(itemBuilder: (_, index) => ProductItem(index: index),);}}
在上面的代碼中,我們將構建器的索引作為構造函數參數傳遞給?ProductItem
?小部件,這種方法有效,但如果ListView
重新構建,它的所有子項也將重新構建。作為替代方法,我們可以在嵌套的ProviderScope
內部覆蓋Provider的值:
// 1. Declare a Providerfinal currentProductIndex = Provider<int>((_) => throw UnimplementedError());class ProductList extends StatelessWidget {@overrideWidget build(BuildContext context) {return ListView.builder(itemBuilder: (context, index) {// 2. Add a parent ProviderScopereturn ProviderScope(overrides: [// 3. Add a dependency override on the indexcurrentProductIndex.overrideWithValue(index),],// 4. return a **const** ProductItem with no constructor argumentschild: const ProductItem(),);});}}class ProductItem extends ConsumerWidget {const ProductItem({super.key});@overrideWidget build(BuildContext context, WidgetRef ref) {// 5. Access the index via WidgetReffinal index = ref.watch(currentProductIndex);// do something with the index}}
在這種情況下:
- 我們創建一個默認拋出
UnimplementedError
的Provider
。 - 通過將父
ProviderScope
添加到ProductItem
小部件來覆蓋其值。 - 我們在
ProductItem
的build
方法中監視索引。
這對性能更有益,因為我們可以將ProductItem
作為const
小部件創建在ListView.builder
中。因此,即使ListView
重新構建,除非其索引發生更改,否則我們的ProductItem
將不會重新構建。
5. HookWidget?- 利用hooks鉤子在無狀態下管理狀態
假設你有一個計數器應用,你使用useState
來管理計數值:
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';class HookWidgetExample extends HookWidget {const HookWidgetExample({super.key});@overrideWidget build(BuildContext context) {// 使用 useState Hook 來管理狀態final counter = useState(0);// 使用 useEffect Hook 處理副作用useEffect(() {debugPrint('HookWidget 初始化或計數器變化: ${counter.value}');return () => debugPrint('HookWidget 清理效果');}, [counter.value]);// 使用 useMemoized 緩存計算結果final doubledValue = useMemoized(() {return counter.value * 2;}, [counter.value]);return Card(child: Padding(padding: const EdgeInsets.all(16.0),child: Column(crossAxisAlignment: CrossAxisAlignment.start,children: [const Text('1. HookWidget',style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),),const SizedBox(height: 10),const Text('表面上是無狀態組件,但實際上是有狀態的'),const SizedBox(height: 10),Text('計數器: ${counter.value}'),Text('雙倍值: $doubledValue'),const SizedBox(height: 10),ElevatedButton(onPressed: () => counter.value++,child: const Text('增加計數'),),],),),);}
}
6. HookConsumerWidget?- 結合 Hooks 和 Riverpod
class HookConsumerWidgetExample extends HookConsumerWidget {const HookConsumerWidgetExample({super.key});@overrideWidget build(BuildContext context, WidgetRef ref) {// 使用 Hooks 管理本地狀態final localCounter = useState(0);final animationController = useAnimationController(duration: const Duration(milliseconds: 500),);// 使用 Riverpod 管理全局狀態final globalCounter = ref.watch(counterProvider);// 使用 useEffect 處理副作用useEffect(() {debugPrint('本地計數器變化: ${localCounter.value}');return null;}, [localCounter.value]);return Card(child: Padding(padding: const EdgeInsets.all(16.0),child: Column(crossAxisAlignment: CrossAxisAlignment.start,children: [const Text('2. HookConsumerWidget',style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),),const SizedBox(height: 10),const Text('結合了 Hooks 和 Riverpod 的強大功能'),const SizedBox(height: 10),Text('本地計數器: ${localCounter.value}'),Text('全局計數器: $globalCounter'),const SizedBox(height: 10),Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly,children: [ElevatedButton(onPressed: () => localCounter.value++,child: const Text('本地+1'),),ElevatedButton(onPressed: () => ref.read(counterProvider.notifier).state++,child: const Text('全局+1'),),],),],),),);}
}