机器翻译正文由机器翻译自项目原始文档(英文),排版经程序统一处理,可能存在偏差,请以原项目仓库为准。
Flutter Hooks
React hooks 的 Flutter 实现: https://medium.com/@dan_abramov/making-sense-of-react-hooks-fdbde8803889
Hooks 是一种管理 Widget 生命周期的新型对象。它们存在的唯一原因是:通过删除重复项来增加组件之间的代码共享。
动机
StatefulWidget 存在一个大问题:很难重用比如 initState 或 dispose 的逻辑。一个明显的例子是 AnimationController:
class Example extends StatefulWidget {
const Example({super.key, required this.duration});
final Duration duration;
@override
_ExampleState createState() => _ExampleState();
}
class _ExampleState extends State<Example> with SingleTickerProviderStateMixin {
late final 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 的小部件都必须几乎从零重新实现所有这些逻辑,这当然是不希望的。
Dart 的 mixin 可以部分解决这个问题,但它们存在其他问题:
- 给定的 mixin 在每个类中只能使用一次。
- Mixin 和类共享同一个对象。这意味着如果两个 mixin 定义了相同名称的变量,结果可能会从编译失败到未知行为不等。
这个库提出了第三种解决方案:
class Example extends HookWidget {
const Example({super.key, required this.duration});
final Duration duration;
@override
Widget build(BuildContext context) {
final controller = useAnimationController(duration: duration);
return Container();
}
}
这个代码在功能上与前面的示例等效。它仍然会处理 AnimationController,并且在 Example.duration 改变时仍会更新其 duration。但你可能会想:
所有逻辑都去哪儿了?
这些逻辑已经被移到了 useAnimationController 中,这是一个直接包含在此库中的函数(参见 现有钩子)——这就是我们所说的 Hook。
钩子是一种具有一些特定性的全新对象类型:
-
它们只能在混入
Hooks的组件的build方法中使用。 -
同一个钩子可以任意次数重复使用。以下代码定义了两个独立的
AnimationController,并且在组件重建时它们能够被正确保留。Widget build(BuildContext context) { final controller = useAnimationController(); final controller2 = useAnimationController(); return Container(); } -
Hooks 完全独立于彼此以及 widget。这意味着它们可以轻松地提取到一个 package 中,并在 pub 上发布供他人使用。
原理
类似于 State,hooks 存储在 Widget 的 Element 中。然而,它不是只有一个 State,而是 Element 存储一个 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();
}
}
关于 hooks 是如何实现的,下面有一篇很棒的文章讲解了它在 React 中的实现方式:https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e
规则
由于 hooks 是通过它们的索引获取的,因此必须遵守一些规则:
始终使用 use 前缀你的 hooks:
Widget build(BuildContext context) {
// starts with `use`, good name
useMyHook();
// doesn't start with `use`, could confuse people into thinking that this isn't a hook
myHook();
// ....
}
无条件地调用钩子
Widget build(BuildContext context) {
useMyHook();
// ....
}
不要将 use 包裹到条件中
Widget build(BuildContext context) {
if (condition) {
useMyHook();
}
// ....
}
关于热重载
由于 hooks 是从它们的索引获取的,人们可能认为在重构时热重载会破坏应用程序。
但不用担心,HookWidget 会覆盖默认的热重载行为以与 hooks 一起工作。不过,在某些情况下,Hook 的状态可能会被重置。
考虑以下 hook 列表:
useA();
useB(0);
useC();
然后考虑在执行热重载之后,我们编辑了 HookB 的参数:
useA();
useB(42);
useC();
这里一切正常,所有钩子都保持其状态。
现在假设我们移除了 HookB。我们现在有:
useA();
useC();
在这种情况下,HookA 保持其状态,但 HookC 会被硬重置。这是因为,当在重构后执行热重载时,所有在第一行受影响之后的 hook 都会被销毁。因此,由于 HookC 放置在 HookB 之后,它将被销毁。
如何创建一个 hook
有两种方式创建 hook:
-
一个函数
函数是编写 hook 最常见的方式。由于 hook 本质上是可组合的,一个函数能够组合其他 hook 来创建一个更复杂的自定义 hook。按惯例,这些函数将以
use作为前缀。以下代码定义了一个自定义 hook,该 hook 创建一个变量,并在每次变量值变化时将其值记录到控制台中:
ValueNotifier<T> useLoggedState<T>([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() { return use(const _TimeAlive()); }以下代码定义了一个钩子,该钩子在
State被释放时打印其存活的总时间。class _TimeAlive extends Hook<void> { const _TimeAlive(); @override _TimeAliveState createState() => _TimeAliveState(); } class _TimeAliveState extends HookState<void, _TimeAlive> { DateTime start; @override void initHook() { super.initHook(); start = DateTime.now(); } @override void build(BuildContext context) {} @override void dispose() { print(DateTime.now().difference(start)); super.dispose(); } }
现有钩子
Flutter_Hooks 已经提供了一系列可重用的 hooks,这些 hooks 被分为不同类型:
原始类型(Primitives)
一组低级 hooks,可与组件的小部件生命周期进行交互
| 名称 | 描述 |
|---|---|
| useEffect | 用于副作用,并可选择性地取消它们。 |
| useState | 创建一个变量并订阅它。 |
| useMemoized | 缓存复杂对象的实例。 |
| useRef | 创建一个包含单一可变属性的对象。 |
| useCallback | 缓存函数实例。 |
| useContext | 获取建筑物 HookWidget 的 BuildContext 。 |
| useValueChanged | 监视一个值,并在其值变化时触发回调。 |
对象绑定(Object-binding)
这一类 hooks 用于通过 hooks 操作现有的 Flutter/Dart 对象。它们会处理对象的创建/更新/销毁。
dart:async 相关的钩子:
| 名称 | 描述 |
|---|---|
| useStream | 订阅一个 Stream 并返回其当前状态作为 AsyncSnapshot 。 |
| useStreamController | 创建一个自动处理销毁的 StreamController 。 |
| useOnStreamChange | 订阅一个 Stream,注册处理程序,并返回 StreamSubscription 。 |
| useFuture | 订阅一个 Future 并返回其当前状态作为 AsyncSnapshot 。 |
与动画相关的钩子:
| 名称 | 描述 |
|---|---|
| useSingleTickerProvider | 创建一个单次使用的 TickerProvider 。 |
| useAnimationController | 创建一个 AnimationController ,它将被自动销毁。 |
| useAnimation | 订阅一个 Animation 并返回其值。 |
与 Listenable 相关的钩子:
| 名称 | 描述 |
|---|---|
| useListenable | 订阅一个 Listenable 并在监听器被调用时将小部件标记为需要重新构建。 |
| useListenableSelector | 类似于 useListenable ,但允许过滤 UI 重建。 |
| useValueNotifier | 创建一个 ValueNotifier ,它将被自动销毁。 |
| useValueListenable | 订阅一个 ValueListenable 并返回其值。 |
| useOnListenableChange | 向一个 Listenable 添加指定的监听器回调,该监听器将在适当时自动移除。 |
杂项钩子:
一系列没有特定主题的钩子。
| 名称 | 描述 |
|---|---|
| useReducer | useState 的替代方案,用于处理更复杂的状态。 |
| usePrevious | 返回上一个调用 [usePrevious] 的参数。 |
| useTextEditingController | Creates a TextEditingController. |
| useFocusNode | Creates a FocusNode. |
| useTabController | 创建并释放一个 TabController 。 |
| useScrollController | 创建并释放一个 ScrollController 。 |
| usePageController | 创建并释放一个 PageController 。 |
| useFixedExtentScrollController | 创建并释放一个 FixedExtentScrollController 。 |
| useAppLifecycleState | 返回当前的 AppLifecycleState ,并在变化时重建组件。 |
| useOnAppLifecycleStateChange | 监听 AppLifecycleState 变化,并在变化时触发回调。 |
| useTransformationController | 创建并释放一个 TransformationController 。 |
| useIsMounted | 钩子版本的 State.mounted 。 |
| useAutomaticKeepAlive | 相当于 AutomaticKeepAlive 小部件的 Hook 版本。 |
| useOnPlatformBrightnessChange | 监听平台 Brightness 变化并在变化时触发回调。 |
| useSearchController | 创建并释放一个 SearchController 。 |
| useWidgetStatesController | 创建并释放一个 WidgetStatesController 。 |
| useExpansibleController | Creates a ExpansibleController. |
| useDebounced | 返回提供值的防抖版本,经过指定超时时间后触发小部件更新。 |
| useDraggableScrollableController | Creates a DraggableScrollableController. |
| useCarouselController | 创建并释放一个 CarouselController 。 |
| useTreeSliverController | Creates a TreeSliverController. |
| useOverlayPortalController | 创建并管理一个 OverlayPortalController 来控制覆盖内容的可见性。当不再需要时,控制器将被自动释放。 |
| useSnapshotController | 创建并管理一个 SnapshotController |
| useCupertinoController | 创建并管理一个 CupertinoController |
贡献
欢迎贡献!
如果您觉得缺少某个 Hook,请随时提交拉取请求。
要合并自定义 Hook,您需要执行以下操作:
-
描述使用案例。
打开一个问题解释我们为什么需要这个钩子,如何使用它,…… 这是很重要的,因为如果这个钩子对大量用户没有吸引力,它是不会被合并的。
如果你的钩子被拒绝,不要担心!被拒绝并不意味着将来如果更多人对此感兴趣,它就无法被合并。与此同时,可以随意将你的钩子发布为 https://pub.dev 上的一个包。
-
为你的钩子编写测试。
除非经过充分测试,否则钩子不会被合并,以避免将来无意中破坏它。
-
将其添加到 README 中并为其编写文档。
- 本文标题:flutter_hooks - Flutter 的 React hooks
- 本文链接:https://www.cn121.com/mobile/rrousselgit-flutter-hooks.html
- 原项目:rrousselGit/flutter_hooks 版权归原作者 rrousselGit 及贡献者所有
- 收录信息:本站于 2026-10-22 收录本项目,本页所列协议与仓库指标均为收录当时的状态;该日期之后原项目的版本更新与协议变更,本页不作同步。
- 开源协议:收录时本项目采用 MIT(查看 LICENSE 原文),本站译文为其衍生内容;使用、修改、分发请以该仓库 LICENSE 原文为准。
- 站点出处:本文首发于 OneTwoOne,收录自 GitHub 开源项目 rrousselGit/flutter_hooks。
- 翻译说明:本页正文为人工智能生成内容——由机器翻译对原项目 README 初译、经程序校验排版,可能存在错漏,请以原项目文档为准。
- 引用声明:商业转载、第三方聚合或 AI 检索训练引用时,请务必保留以上来源出处、本文永久链接,以及原项目的版权声明与许可信息。
- 下架通道:若原项目此后变更或收紧了许可协议、或作者/权利人认为本站的收录方式(译文、排版适配、简介翻译等)超出其授权范围,请通过 xyd3302001@163.com 发送下架通知,并附上项目地址与本页链接。本站核实后将第一时间删除本页内容,或改为不复制原文的目录性收录;署名更正等其他要求可一并提出。