Flutter Hooks

Hooks 是一类新的对象,用于管理 Widget 的生命周期。它们的存在只有一个原因:增加 Widget 之间的代码共享,并完全替代 StatefulWidget。

Flutter 的 React Hooks。Hooks 是一类新的对象,用于管理 Widget 的生命周期。它们用于增加 Widget 之间的代码共享,并完全替代 StatefulWidget。

动机

StatefulWidget 存在一个大问题:重用诸如 initStatedispose 之类的逻辑非常困难。一个明显的例子是 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 是具有一些特殊性的新类对象

  • 它们只能在 HookWidgetbuild 方法中使用。
  • 同一个 hook 可以无限次地重复使用
    以下代码定义了两个独立的 AnimationController,在 Widget 重建时,它们能够被正确地保留。
Widget build(BuildContext context) {
  final controller = useAnimationController();
  final controller2 = useAnimationController();
  return Container();
}
  • Hooks 完全独立于彼此以及 Widget。这意味着它们可以轻松地提取到包中并在 pub 上发布供他人使用。

原则

State 类似,hooks 存储在 WidgetElement 上。但 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 非常相似,并可以访问生命周期和方法,如 initHookdisposesetState。通常,将类隐藏在函数下是一种好习惯,如下所示:

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 获取正在构建的 HookWidgetBuildContext
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] 的前一个参数。

GitHub

https://github.com/rrousselGit/flutter_hooks