Flutter之riverpod状态管理Widget UI详解
一、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'),),],),],),),);}
}