关于 Hooks(钩子)
本页介绍了什么是 Hooks 以及它们与 Riverpod 的关系。
"Hooks" 是独立于 Riverpod 的单独包中常见的实用程序:flutter_hooks。
虽然 flutter_hooks 是一个完全独立的包,
并且与 Riverpod 没有任何关系(至少没有直接关系),
但通常将 Riverpod 和 flutter_hooks 配对在一起。
你应该使用 hooks 吗?
Hooks 是一个强大的工具,但并不适合所有人。
如果您是 Riverpod 的新手,您可能应该避免使用 hooks。
虽然 hooks 很有用,但对于 Riverpod 来说并不是必需的。
您不应该为了 Riverpod 开始使用 hooks。
相反,您开始使用 hooks,是因为您想使用 hooks。
使用 hooks 是一种权衡。它们非常适合生成健壮且可重用的代码, 但它们也是一个需要学习的新概念,一开始可能会令人困惑。 Hooks 不是 Flutter 的核心概念。因此,它们在 Flutter/Dart 中会感觉格格不入。
什么是 Hooks?
Hooks 是小部件内部使用的函数。它们被设计为 StatefulWidget 的替代品, 以使逻辑更加可重用和可组合。
Hooks 是来自 React 的一个概念,flutter_hooks
只是 React 实现到 Flutter 的一个端口。
因此,是的,hooks 在 Flutter 中可能感觉有点不合适。理想情况下,
未来我们会有一个专门为 Flutter 设计的 Hooks 解决问题的解决方案。
如果 Riverpod 的提供者程序用于“全局”应用程序状态,则 Hooks 用于本地小部件状态。
Hooks 通常用于处理有状态的 UI 对象,例如 TextEditingController、
AnimationController。
它们还可以作为“构建器”模式的替代品,用不涉及“嵌套”的替代方案替换诸如
FutureBuilder/TweenAnimatedBuilder
之类的小部件,从而大大提高可读性。
一般来说,钩子有助于:
- 表单
- 动画
- 对用户事件做出反应
- ……
例如,我们可以使用钩子手动实现淡入动画,其中小部件开始不可见并慢慢出现。
如果我们使用 StatefulWidget,代码将如下所示:
class FadeIn extends StatefulWidget {
const FadeIn({Key? key, required this.child}) : super(key: key);
final Widget child;
State<FadeIn> createState() => _FadeInState();
}
class _FadeInState extends State<FadeIn> with SingleTickerProviderStateMixin {
late final AnimationController animationController = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
);
void initState() {
super.initState();
animationController.forward();
}
void dispose() {
animationController.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: animationController,
builder: (context, child) {
return Opacity(
opacity: animationController.value,
child: widget.child,
);
},
);
}
}
使用 hooks,相当于:
class FadeIn extends HookWidget {
const FadeIn({Key? key, required this.child}) : super(key: key);
final Widget child;
Widget build(BuildContext context) {
// 创建一个 AnimationController。
// 卸载 widget 时,控制器将自动处置。
final animationController = useAnimationController(
duration: const Duration(seconds: 2),
);
// useEffect 相当于 initState + didUpdateWidget + dispose。
// 传给 useEffect 的回调会在第一次调用钩子时执行,
// 然后每当作为第二个参数传递的列表发生变化时也会执行。
// 由于我们在这里传递的是一个空的常量列表,
// 因此严格意义上等同于 `initState`。
useEffect(() {
// 在首次呈现 widget 时启动动画。
animationController.forward();
// 我们可以选择在这里返回一些“处置”逻辑
return null;
}, const []);
// 告诉 Flutter 在动画更新时重建此部件。
// 这相当于 AnimatedBuilder
useAnimation(animationController);
return Opacity(
opacity: animationController.value,
child: child,
);
}
}
这段代码中有一些有趣的事情需要注意:
- 不存在内存泄漏。每当小部件重建时,此代码都不会重新创建新的
AnimationController
, 并且在卸载小部件时正确处置控制器。
在同一个小部件中可以根据需要多次使用钩子。 因此,如果我们愿意,我们可以创建多个
AnimationController
:
Widget build(BuildContext context) {
final animationController = useAnimationController(
duration: const Duration(seconds: 2),
);
final anotherController = useAnimationController(
duration: const Duration(seconds: 2),
);
...
}这会创建两个控制器,不会产生任何负面后果。
如果我们愿意,我们可以将此逻辑重构为一个单独的可重用函数:
double useFadeIn() {
final animationController = useAnimationController(
duration: const Duration(seconds: 2),
);
useEffect(() {
animationController.forward();
return null;
}, const []);
useAnimation(animationController);
return animationController.value;
}然后我们可以在我们的小部件中使用这个函数,只要该小部件是 HookWidget:
class FadeIn extends HookWidget {
const FadeIn({Key? key, required this.child}) : super(key: key);
final Widget child;
Widget build(BuildContext context) {
final fade = useFadeIn();
return Opacity(opacity: fade, child: child);
}
}请注意我们的
useFadeIn
函数是如何完全独立于我们的FadeIn
小部件的。
如果我们愿意,我们可以在完全不同的小部件中使用该useFadeIn
函数,并且它仍然可以工作!
hooks 的规则
Hooks 具有独特的约束:
它们只能在扩展 HookWidget 的小部件的
build
方法中使用:好:
class Example extends HookWidget {
Widget build(BuildContext context) {
final controller = useAnimationController();
...
}
}坏:
// 不是 HookWidget
class Example extends StatelessWidget {
Widget build(BuildContext context) {
final controller = useAnimationController();
...
}
}坏:
class Example extends HookWidget {
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
// _实际上_不是在 "build" 方法中,
// 而是在用户交互生命周期中(这里是 "按下时")。
final controller = useAnimationController();
},
child: Text('click me'),
);
}
}
它们不能在条件语句或在循环语句中使用。
坏:
class Example extends HookWidget {
const Example({required this.condition, super.key});
final bool condition;
Widget build(BuildContext context) {
if (condition) {
// 不应该在 "if"/"for"/... 中使用 Hooks
final controller = useAnimationController();
}
...
}
}
有关钩子的更多信息,请参阅 flutter_hooks。
Hooks 和 Riverpod
安装
由于 Hooks 与 Riverpod 是独立的,因此需要单独安装 Hooks。 如果你想使用它们,安装 hooks_riverpod 是不够的。 您仍然需要将 flutter_hooks 添加到您的依赖项中。 请参阅 入门指南 了解更多信息。
用途
在某些情况下,您可能想要编写一个同时使用 hooks 和 Riverpod 的 Widget。
但您可能已经注意到,Hooks 和 Riverpod 都提供了自己的
自定义小部件基本类型:HookWidget 和 ConsumerWidget。
但类一次只能扩展一个父类。
为了解决这个问题,你可以使用 hooks_riverpod 包。
该包提供了一个 HookConsumerWidget 类,
它将 HookWidget 和 ConsumerWidget 组合成一个类型。
因此,您可以继承 HookConsumerWidget 而不是 HookWidget:
// We extend HookConsumerWidget instead of HookWidget
class Example extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
// We can use both hooks and providers here
final counter = useState(0);
final value = ref.watch(myProvider);
return Text('Hello $counter $value');
}
}
或者,您可以使用两个包提供的“构建器 builder”。
例如,我们可以坚持使用 StatelessWidget
,
并同时使用 HookBuilder
和 Consumer
。
class Example extends StatelessWidget {
Widget build(BuildContext context) {
// We can use the builders provided by both packages
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 来简化它, 它是两个构建器的组合:
class Example extends StatelessWidget {
Widget build(BuildContext context) {
// Equivalent to using both Consumer and HookBuilder.
return HookConsumer(
builder: (context, ref, child) {
final counter = useState(0);
final value = ref.watch(myProvider);
return Text('Hello $counter $value');
},
);
}
}