跳到主要内容

关于 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 对象,例如 TextEditingControllerAnimationController
它们还可以作为“构建器”模式的替代品,用不涉及“嵌套”的替代方案替换诸如 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 都提供了自己的 自定义小部件基本类型:HookWidgetConsumerWidget
但类一次只能扩展一个父类。

为了解决这个问题,你可以使用 hooks_riverpod 包。 该包提供了一个 HookConsumerWidget 类, 它将 HookWidgetConsumerWidget 组合成一个类型。
因此,您可以继承 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, 并同时使用 HookBuilderConsumer


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');
},
);
}
}