Flutter Hooks
Hooks 是一类新的对象,用于管理 Widget 的生命周期。它们的存在只有一个原因:增加 Widget 之间的代码共享,并完全替代 StatefulWidget。
Flutter 的 React Hooks。Hooks 是一类新的对象,用于管理 Widget 的生命周期。它们用于增加 Widget 之间的代码共享,并完全替代 StatefulWidget。
动机
StatefulWidget 存在一个大问题:重用诸如 initState 或 dispose 之类的逻辑非常困难。一个明显的例子是 AnimationController
class Example extends StatefulWidget {
final Duration duration;
const Example({Key key, @required this.duration})
: assert(duration != null),
super(key: key);
@override
_ExampleState createState() => _ExampleState();
}
class _ExampleState extends State<Example> with SingleTickerProviderStateMixin {
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: widget.duration);
}
@override
void didUpdateWidget(Example oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.duration != oldWidget.duration) {
_controller.duration = widget.duration;
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container();
}
}
所有希望使用 AnimationController 的 Widget 都必须从头开始重新实现几乎所有这些功能,这当然是不理想的。
Dart 的 mixins 可以部分解决这个问题,但它们存在其他问题
- 给定的 mixin 在每个类中只能使用一次。
- Mixin 和类共享同一个对象。这意味着,如果两个 mixin 定义了同名的变量,最终结果可能从编译失败到未知行为不等。
此库提出第三种解决方案
class Example extends HookWidget {
final Duration duration;
const Example({Key key, @required this.duration})
: assert(duration != null),
super(key: key);
@override
Widget build(BuildContext context) {
final controller = useAnimationController(duration: duration);
return Container();
}
}
此代码与前面的示例严格等效。它仍然会处置 AnimationController,并在 `Example.duration` 更改时更新其 duration。
但你可能在想
所有逻辑去哪儿了?
那些逻辑已移至 useAnimationController,这是一个直接包含在此库中的函数(请参阅 https://github.com/rrousselGit/flutter_hooks#existing-hooks)。这就是我们所说的Hook。
Hooks 是具有一些特殊性的新类对象
- 它们只能在
HookWidget的build方法中使用。 - 同一个 hook 可以无限次地重复使用
以下代码定义了两个独立的AnimationController,在 Widget 重建时,它们能够被正确地保留。
Widget build(BuildContext context) {
final controller = useAnimationController();
final controller2 = useAnimationController();
return Container();
}
- Hooks 完全独立于彼此以及 Widget。这意味着它们可以轻松地提取到包中并在 pub 上发布供他人使用。
原则
与 State 类似,hooks 存储在 Widget 的 Element 上。但 Element 不只有一个 State,而是存储一个 List<Hook>。然后,要使用 Hook,必须调用 Hook.use。
use 返回的 hook 基于它被调用的次数。第一次调用返回第一个 hook;第二次调用返回第二个 hook,第三次返回第三个 hook,依此类推。
如果这仍然不清楚,hooks 的一个简单实现如下
class HookElement extends Element {
List<HookState> _hooks;
int _hookIndex;
T use<T>(Hook<T> hook) => _hooks[_hookIndex++].build(this);
@override
performRebuild() {
_hookIndex = 0;
super.performRebuild();
}
}
有关它们如何实现的更多解释,这是一篇关于它们在 React 中如何实现的出色文章:https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e
规则
由于 hooks 是根据它们的索引获取的,因此必须遵守一些规则
请无条件调用 use
Widget build(BuildContext context) {
Hook.use(MyHook());
// ....
}
请勿将 use 包装在条件语句中
Widget build(BuildContext context) {
if (condition) {
Hook.use(MyHook());
}
// ....
}
请始终调用所有 hooks
Widget build(BuildContext context) {
Hook.use(Hook1());
Hook.use(Hook2());
// ....
}
请勿在调用所有 hooks 之前中止 build 方法
Widget build(BuildContext context) {
Hook.use(Hook1());
if (condition) {
return Container();
}
Hook.use(Hook2());
// ....
}
关于热重载
由于 hooks 是根据它们的索引获取的,因此人们可能会认为在重构期间进行热重载会破坏应用程序。
但请放心,HookWidget 会覆盖默认的热重载行为以与 hooks 一起使用。尽管如此,在某些情况下,Hook 的状态可能会被重置。
考虑以下 hook 列表
Hook.use(HookA());
Hook.use(HookB(0));
Hook.use(HookC(0));
然后考虑在热重载之后,我们编辑了 HookB 的参数
Hook.use(HookA());
Hook.use(HookB(42));
Hook.use(HookC());
这里一切正常;所有 hooks 都保留了它们的状态。
现在考虑我们删除了 HookB。我们现在有
Hook.use(HookA());
Hook.use(HookC());
在这种情况下,HookA 保留了它的状态,但 HookC 被硬重置了。
发生这种情况是因为当进行重构时,受影响的第一行之后的所有 hooks 都会被处置。由于 HookC 位于 HookB 之后,因此它被处置了。
如何使用
有两种创建 hook 的方法
- 函数
函数是编写 hook 最常见的方式。由于 hooks 本质上是可组合的,因此一个函数将能够组合其他 hooks 来创建一个自定义 hook。按照惯例,这些函数将以 use 作为前缀。
以下定义了一个自定义 hook,它创建一个变量并在该变量更改时将其值记录到控制台。
ValueNotifier<T> useLoggedState<T>(BuildContext context, [T initialData]) {
final result = useState<T>(initialData);
useValueChanged(result.value, (_, __) {
print(result.value);
});
return result;
}
- 类
当 hook 变得过于复杂时,可以将其转换为扩展 Hook 的类,然后使用 Hook.use 来使用它。作为一个类,hook 的外观将与 State 非常相似,并可以访问生命周期和方法,如 initHook、dispose 和 setState。通常,将类隐藏在函数下是一种好习惯,如下所示:
Result useMyHook(BuildContext context) {
return Hook.use(_MyHook());
}
以下定义了一个 hook,它打印一个 State 存活的时间。
class _TimeAlive<T> extends Hook<void> {
const _TimeAlive();
@override
_TimeAliveState<T> createState() => _TimeAliveState<T>();
}
class _TimeAliveState<T> extends HookState<void, _TimeAlive<T>> {
DateTime start;
@override
void initHook() {
super.initHook();
start = DateTime.now();
}
@override
void build(BuildContext context) {
// this hook doesn't create anything nor uses other hooks
}
@override
void dispose() {
print(DateTime.now().difference(start));
super.dispose();
}
}
现有 hooks
Flutter_hooks 附带了一系列可重用的 hook。
它们分为几类
基本类型
一组低级 hook,用于与 Widget 的各种生命周期进行交互
| 名称 | 描述 |
|---|---|
| useEffect | 用于副作用和可选的取消它们。 |
| useState | 创建变量并订阅它。 |
| useMemoized | 缓存复杂对象的实例。 |
| useContext | 获取正在构建的 HookWidget 的 BuildContext。 |
| useValueChanged | 监视一个值,并在值更改时调用回调。 |
对象绑定
这一类 hooks 允许使用 hooks 来操作现有的 Flutter/Dart 对象。
它们将负责创建/更新/处置对象。
dart:async 相关
| 名称 | 描述 |
|---|---|
| useStream | 订阅 Stream 并以 AsyncSnapshot 的形式返回其当前状态。 |
| useStreamController | 创建一个自动处置的 StreamController。 |
| useFuture | 订阅 Future 并以 AsyncSnapshot 的形式返回其当前状态。 |
动画相关
| 名称 | 描述 |
|---|---|
| useSingleTickerProvider | 创建一个仅使用一次的 TickerProvider。 |
| useAnimationController | 创建一个自动处置的 AnimationController。 |
| useAnimation | 订阅 Animation 并返回其值。 |
Listenable 相关
| 名称 | 描述 |
|---|---|
| useListenable | 订阅 Listenable 并标记 Widget 需要在调用监听器时重建。 |
| useValueNotifier | 创建一个自动处置的 ValueNotifier。 |
| useValueListenable | 订阅 ValueListenable 并返回其值。 |
杂项
一系列没有特定主题的 hooks。
| 名称 | 描述 |
|---|---|
| useReducer | 用于更复杂状态的 useState 的替代方案。 |
| usePrevious | 返回调用 [usePrevious] 的前一个参数。 |