pub:riverpod | Dart Package (flutter-io.cn)
译时版本: 2.4.9
之前翻译过 Riverpod 的官方文档,现在随着版本更新,官方文档又多了很多新内容,所以再补充翻译一下。
之前翻译过的内容,现在官方文档有中文了。
Flutter状态管理库Riverpod官方文档翻译汇总 - 掘金 (juejin.cn)
关于钩子
该篇将说明什么是钩子和它们如何与 Riverpod 关联。
"钩子" 是一个独立包里的通用工具类,该包独立于 Riverpod:flutter_hooks 。 虽然 flutter_hooks 是一个完全独立的包,并且和 Riverpod 没有什么关系(至少没有直接关系),将 Riverpod 和 flutter_hooks 配对使用也是一个常用做法。
是否应该使用钩子?
钩子是一个强大的工具,但是不是对所有人。
如果是 Riverpod 的新人,很可能应该避免使用钩子。
虽然很有用,钩子对于 Riverpod 也不是必须的。
不应该是因为使用 Riverpod 而使用钩子。反而应该是想要使用钩子而使用钩子。
使用钩子是一种权衡。它们对于产出健壮或重用的代码来说是强大的,但是它们也有一些新概念需要学习,并且开始时会容易产生困扰。钩子不是核心的 Flutter 概念。因此,它们在 Flutter/Dart 没有相关内容。
钩子是什么?
钩子是在组件内部使用的函数。它们是设计作为 StatefulWidget (有状态组件)的替代方案,使业务逻辑可重用或组合。
钩子是来自于 React 的概念,[flutter_hooks] 只不过是 React 上的实现到 Flutter 的移植。
因此,是的,在 Flutter 上钩子可能感觉不太适合。理想地,将来会有一个方案来解决钩子解决的问题,会特别针对 Flutter 进行设计。
如果 Riverpod 的 provider 是用于"全局"应用状态,钩子则用于本地组件状态。钩子是典型地用于处理有状态的 UI 对象,如 TextEditingController, AnimationController。
它们也能作为"builder"模式的替代品替换组件,如FutureBuilder/TweenAnimatedBuilder ,通过不带"嵌套"的做法 - 大大提高了可读性。
通常,钩子有助于以下处理:
- 表单
- 动画
- 用户事件响应
- 。。。
作为示例,可以使用钩子手动实现一个渐入动画,即使组件以不可见开始,然后慢慢出现。
如果使用 StatefulWidget ,代码可能如下:
scala
class FadeIn extends StatefulWidget {
const FadeIn({Key? key, required this.child}) : super(key: key);
final Widget child;
@override
State<FadeIn> createState() => _FadeInState();
}
class _FadeInState extends State<FadeIn> with SingleTickerProviderStateMixin {
late final AnimationController animationController = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
);
@override
void initState() {
super.initState();
animationController.forward();
}
@override
void dispose() {
animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: animationController,
builder: (context, child) {
return Opacity(
opacity: animationController.value,
child: widget.child,
);
},
);
}
}
使用钩子,相同的处理代码会如下:
scala
class FadeIn extends HookWidget {
const FadeIn({Key? key, required this.child}) : super(key: key);
final Widget child;
@override
Widget build(BuildContext context) {
// 创建 AnimationController 。该 controller 会在组件卸载后自动清除。
final animationController = useAnimationController(
duration: const Duration(seconds: 2),
);
// useEffect 等同于 initState + didUpdateWidget + dispose 。
// 传递给 useEffect 的回调会在钩子第一次被调用时执行,然后会在列表作为第二个参数被传递时执行。
// 因为这里传递空的常量列表,就完全等同于 `initState` 。
useEffect(() {
// 在组件第一次渲染时启动动画。
animationController.forward();
// 这里可以返回一些"清除"逻辑作为可选处理。
return null;
}, const []);
// 告诉 Flutter 在动画更新时重建组件。
// 这等同于 AnimatedBuilder
useAnimation(animationController);
return Opacity(
opacity: animationController.value,
child: child,
);
}
}
代码中有一些有趣的内容需要注意:
-
没有内存泄露。即使组件重新构建,代码也不会再次创建一个新的
AnimationController
,然后 controller 会在组件卸载后正确地被释放。 -
在相同的组件中,钩子想用多少次就能用多少次。因此,想创建多个
AnimationController
就可以创建多个。less@override Widget build(BuildContext context) { final animationController = useAnimationController( duration: const Duration(seconds: 2), ); final anotherController = useAnimationController( duration: const Duration(seconds: 2), ); ... }
这会创建两个 controller ,不会有任何不好的后果。
-
如果需要,可以将该业务逻辑重构为一个独立可重用的函数:
dartdouble useFadeIn() { final animationController = useAnimationController( duration: const Duration(seconds: 2), ); useEffect(() { animationController.forward(); return null; }, const []); useAnimation(animationController); return animationController.value; }
之后可以在组件内部使用该函数,只要该组件是 HookWidget :
dartclass FadeIn extends HookWidget { const FadeIn({Key? key, required this.child}) : super(key: key); final Widget child; @override Widget build(BuildContext context) { final fade = useFadeIn(); return Opacity(opacity: fade, child: child); } }
注意
useFadeIn
函数和FadeIn
组件是完全独立的。如果需要,可以在一个完全不同的组件中使用
useFadeIn
函数,它仍然能正常工作!
钩子的规则
钩子有一些独有的限制:
-
它们只能在继承 HookWidget 的组件的
build
方法中使用:好:
scalaclass Example extends HookWidget { @override Widget build(BuildContext context) { final controller = useAnimationController(); ... } }
不好:
scala// 不是 HookWidget class Example extends StatelessWidget { @override Widget build(BuildContext context) { final controller = useAnimationController(); ... } }
不好:
scalaclass Example extends HookWidget { @override Widget build(BuildContext context) { return ElevatedButton( onPressed: () { // _actually_ 不在 "build" 方法中,但是在用户交互生命周期(这里是"on pressed")中。 final controller = useAnimationController(); }, child: Text('click me'), ); } }
-
不能在条件分支或循环中使用。
不好:
scalaclass Example extends HookWidget { const Example({required this.condition, super.key}); final bool condition; @override Widget build(BuildContext context) { if (condition) { // Hooks 不应该在 "if"/"for" 中使用。。。 final controller = useAnimationController(); } ... } }
更多关于钩子的信息,查看 flutter_hooks 。
钩子 和 Riverpod
安装
因为钩子是独立于 Riverpod 的,所以需要单独安装钩子。如果想使用它们,安装 hooks_riverpod 就足够了。如果仍需要安装 flutter_hooks 到依赖中。查看开始使用获取更多信息。
用法
一些情况下,可能想编写同时使用钩子和 Riverpod 的组件。但是可能已经多次注意到,钩子和 Riverpod 分别提供了各自的自定义组件的基类:HookWidget 和 ConsumerWidget。
但是类一次只能继承一个父类。
要解决该问题,可以使用 hooks_riverpod 包。该包提供了一个 HookConsumerWidget 类,它将 HookWidget 和 ConsumerWidget 绑定为了单个类型。
因此就可以编写 HookConsumerWidget 的子类来代替继承 HookWidget :
dart
// 继承 HookConsumerWidget 代替继承 HookWidget
class Example extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// 在这里可以同时使用钩子和 provider
final counter = useState(0);
final value = ref.watch(myProvider);
return Text('Hello $counter $value');
}
}
另一个替代方案,可以使用两个包提供的 "builder" 。
例如,可以继续使用 StatelessWidget
,然后使用 HookBuilder
和 Consumer
。
dart
class Example extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 同时使用两个包的 builder
return Consumer(
builder: (context, ref, child) {
return HookBuilder(builder: (context) {
final counter = useState(0);
final value = ref.watch(myProvider);
return Text('Hello $counter $value');
});
},
);
}
}
注意
该方式无需使用 hooks_riverpod 。只需要 flutter_riverpod 。
如果喜欢这种方式,hooks_riverpod 会提供 HookConsumer 以提高效率,它同时绑定了两个 builder :
dart
class Example extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 等同于同时使用 Consumer 和 HookBuilder 。
return HookConsumer(
builder: (context, ref, child) {
final counter = useState(0);
final value = ref.watch(myProvider);
return Text('Hello $counter $value');
},
);
}
}