深色模式
Flutter Hooks 官方 API 说明
概述
flutter_hooks 是把 Hooks 模式带到 Flutter 的一个库。它的核心目标不是“替代所有 StatefulWidget”,而是把一类经常重复出现的生命周期逻辑抽成可复用函数。
官方文档给出的典型例子是 AnimationController。如果用传统 StatefulWidget,你通常要自己写:
initStatedidUpdateWidgetdispose
而用 Hooks 后,很多这类对象都可以直接通过对应的 useXxx() 创建、更新和自动释放。
如果从 API 角度看,Flutter Hooks 主要分成几类:
- Primitives
dart:asyncrelated hooks- Animation related hooks
- Listenable related hooks
- Misc hooks
本文以 flutter_hooks 官方文档和 pub.dev 最新稳定版说明为准来整理。
安装
最直接的安装方式是:
sh
flutter pub add flutter_hooks然后在代码中导入:
dart
import 'package:flutter_hooks/flutter_hooks.dart';如果你同时在用 Riverpod,对应常见写法通常是:
- 只用 Hooks:继承
HookWidget - Hooks + Riverpod:继承
HookConsumerWidget或StatefulHookConsumerWidget
Hooks 基本规则
Hooks 的底层是按调用顺序记录的,所以规则比普通函数更严格。
只在支持 Hooks 的组件里调用
最常见的是 HookWidget:
dart
class CounterPage extends HookWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context) {
final count = useState(0);
return Text('${count.value}');
}
}如果组件本身不支持 hooks,就不能直接在 build 里调用 useState()、useEffect() 这类函数。
Hook 名称以 use 开头
官方约定所有 hook 都以 use 开头,这样看到函数名时就能知道它不是普通工具函数。
dart
useMyHook();Hook 必须无条件调用
可以:
dart
Widget build(BuildContext context) {
useMyHook();
return const SizedBox();
}不可以:
dart
Widget build(BuildContext context) {
if (condition) {
useMyHook();
}
return const SizedBox();
}同样也不要把 hook 放进循环、回调或其他不稳定调用路径里。原因很简单:一旦调用顺序变了,hook 的状态映射就会错位。
Hot reload 不会完全乱,但顺序变更后状态可能重置
比如原来是:
dart
useA();
useB(0);
useC();如果只是改参数:
dart
useA();
useB(42);
useC();通常状态还能保持。
但如果直接删掉中间的 hook:
dart
useA();
useC();那么受影响位置之后的 hook 可能会被重置。因此重构 hooks 顺序时,要预期 hot reload 后局部状态会丢失。
如何创建自定义 Hook
官方文档提到两种方式:函数式 hook 和类式 hook。
函数式 hook
这是最常见的写法,本质上就是用已有 hooks 组合出一个新的 hook。
dart
ValueNotifier<T> useLoggedState<T>(T initialData) {
final state = useState(initialData);
useValueChanged(state.value, (_, __) {
debugPrint('state changed: ${state.value}');
});
return state;
}适合:
- 抽离一组重复的页面状态逻辑
- 组合多个 hooks 形成一个业务能力
- 隐藏初始化、订阅、清理等细节
类式 hook
当 hook 逻辑很复杂,或者你需要更接近 State 的生命周期能力时,也可以继承 Hook / HookState。
不过日常业务里,绝大多数场景用函数式 hook 就够了。
Primitives
这一组是最基础的 hooks,很多其他 hook 的思路都能从它们身上看出来。
useEffect
useEffect 用来处理副作用,并且可以返回一个清理函数。
官方文档强调两点:
useEffect默认会在每次build时同步调用- 如果传了
keys,则只会在首次调用以及依赖变化时再次调用
dart
class MessagePage extends HookWidget {
const MessagePage({super.key, required this.stream});
final Stream<String> stream;
@override
Widget build(BuildContext context) {
final latestMessage = useState('');
useEffect(() {
final subscription = stream.listen((value) {
latestMessage.value = value;
});
return subscription.cancel;
}, [stream]);
return Text(latestMessage.value);
}
}常见场景:
- 订阅
Stream - 监听外部对象并在销毁时清理
- 把某个外部值同步到控制器
注意点:
effect本身不要直接写成async- 返回的清理函数会在依赖变化后重新执行 effect 时,或者组件销毁时调用
- 如果依赖对象是每次
build都新建的,useEffect也会一直重新执行
useState
useState 用来创建一个简单状态,返回值是 ValueNotifier<T>。
dart
class CounterPage extends HookWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context) {
final count = useState(0);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('count: ${count.value}'),
ElevatedButton(
onPressed: () {
count.value++;
},
child: const Text('加一'),
),
],
);
}
}常见场景:
- 页面里的布尔开关
- 输入框以外的临时本地状态
- 很轻量的交互计数、选中态、折叠态
注意点:
initialData只在第一次调用时生效,后续 rebuild 不会重新初始化- 状态简单时优先用它,复杂状态再考虑
useReducer
useMemoized
useMemoized 用来缓存一个对象实例,避免每次 build 都重复创建。
dart
class UserPage extends HookWidget {
const UserPage({super.key, required this.userId});
final int userId;
@override
Widget build(BuildContext context) {
final future = useMemoized(() {
return fetchUser(userId);
}, [userId]);
final snapshot = useFuture(future);
if (!snapshot.hasData) {
return const CircularProgressIndicator();
}
return Text(snapshot.data!.name);
}
}常见场景:
- 缓存
Future - 缓存仓库对象、解析器、控制器以外的复杂实例
- 避免某段昂贵初始化逻辑在每次
build时重复执行
注意点:
- 第一次调用时会立刻执行
valueBuilder - 只有
keys改变时,才会重新创建实例
useRef
useRef 用来保存一个可变引用对象,适合存储“变化了但不希望触发重建”的值。
dart
class ClickPage extends HookWidget {
const ClickPage({super.key});
@override
Widget build(BuildContext context) {
final clickCount = useRef(0);
return ElevatedButton(
onPressed: () {
clickCount.value++;
debugPrint('clicked: ${clickCount.value}');
},
child: const Text('点击'),
);
}
}常见场景:
- 保存上一次时间戳、计数器、缓存句柄
- 保存不需要触发 UI 重建的临时值
- 配合
useEffect存储外部引用
useCallback
useCallback 用来缓存函数实例。
dart
final onSubmit = useCallback(() {
debugPrint('submit');
}, const []);适合把回调传给子组件,或者依赖某些参数生成稳定函数引用时使用。它的思路和 useMemoized 类似,只是缓存对象从普通值换成了函数。
useContext
useContext 用来获取当前 HookWidget 的 BuildContext。
dart
final context = useContext();它通常不如直接使用 build(BuildContext context) 常见,但在自定义 hook 内部想拿到上下文时很有用。
useValueChanged
useValueChanged 用来监听某个值的变化,并在变化时执行回调。
dart
useValueChanged(keyword, (_, __) {
debugPrint('keyword changed: $keyword');
});它适合做“值变化后触发一个动作”,但不直接承担 UI 状态本身。
dart:async related hooks
这一组 hook 主要围绕 Future、Stream 和 StreamController。
useFuture
useFuture 用来订阅一个 Future,并返回 AsyncSnapshot<T>。
dart
class ProfilePage extends HookWidget {
const ProfilePage({super.key, required this.userId});
final int userId;
@override
Widget build(BuildContext context) {
final future = useMemoized(() => fetchUser(userId), [userId]);
final snapshot = useFuture(future);
if (snapshot.connectionState != ConnectionState.done) {
return const CircularProgressIndicator();
}
if (snapshot.hasError) {
return Text('加载失败:${snapshot.error}');
}
return Text(snapshot.data!.name);
}
}常见场景:
- 页面首次加载一次数据
- 根据参数变化重新请求数据
- 和
useMemoized配合缓存异步任务
注意点:
- 官方明确建议不要在
useFuture里直接创建Future - 正确写法通常是先
useMemoized(() => future),再useFuture(future) preserveState默认为true,切换Future时会尽量保留旧状态
useStream
useStream 用来订阅一个 Stream,返回 AsyncSnapshot<T>。
dart
class ClockText extends HookWidget {
const ClockText({super.key});
@override
Widget build(BuildContext context) {
final stream = useMemoized(() {
return Stream.periodic(const Duration(seconds: 1), (_) {
return DateTime.now();
});
});
final snapshot = useStream(stream);
return Text(
snapshot.data?.toIso8601String() ?? 'loading...',
);
}
}常见场景:
- WebSocket 或消息流
- 定时器流
- 本地事件流、播放进度流、下载进度流
注意点:
preserveState默认为true- 如果你切换的是多个不同来源的 stream,需要留意保留旧状态是否符合预期
useStreamController
useStreamController 用来创建并自动销毁 StreamController。
dart
final controller = useStreamController<String>();适合你需要在组件里手动发流,但又不想自己管理 dispose 时使用。
useOnStreamChange
useOnStreamChange 用来订阅一个 Stream,注册监听回调,并返回对应的 StreamSubscription。
dart
useOnStreamChange<String>(
stream,
onData: (value) => debugPrint(value),
);如果你只是想监听流并做副作用,而不是拿 AsyncSnapshot 去渲染 UI,这个 hook 会比 useStream 更直接。
Animation related hooks
这一组 hook 主要把动画相关对象的创建、更新和销毁封装好了。
useAnimationController
useAnimationController 用来创建 AnimationController,并在合适的时候自动销毁。
dart
class FadeBox extends HookWidget {
const FadeBox({super.key});
@override
Widget build(BuildContext context) {
final controller = useAnimationController(
duration: const Duration(milliseconds: 300),
);
final opacity = useAnimation(
Tween<double>(begin: 0, end: 1).animate(controller),
);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Opacity(
opacity: opacity,
child: const FlutterLogo(size: 72),
),
ElevatedButton(
onPressed: () {
controller.forward(from: 0);
},
child: const Text('播放动画'),
),
],
);
}
}常见场景:
- 淡入淡出
- 缩放、位移、进度条
- 任何需要
AnimationController的自定义动效
注意点:
- 不传
vsync时,官方会隐式通过useSingleTickerProvider处理 duration改变时会自动同步到 controllerinitialValue、lowerBound、upperBound等参数只在首次调用时生效
useSingleTickerProvider
useSingleTickerProvider 用来获取单个 TickerProvider。
dart
final vsync = useSingleTickerProvider();一般只有你要手动把 vsync 传给别的动画对象时才需要直接用它;多数情况下直接用 useAnimationController 就够了。
useAnimation
useAnimation 用来监听一个 Animation<T>,并直接返回当前值。
dart
final value = useAnimation(animation);适合在 UI 中直接消费动画当前值,而不是自己给动画加 listener。
Listenable related hooks
这一组 hook 用来处理 Listenable、ValueNotifier、ValueListenable 这类对象。
useListenable
useListenable 用来订阅一个 Listenable,当它触发监听时让当前 Widget 重建。
dart
useListenable(animationController);适合你已经有一个 Listenable 对象,只需要跟着它刷新 UI 的场景。
useListenableSelector
useListenableSelector 和 useListenable 类似,但支持只选择你关心的那部分值,从而减少不必要重建。
dart
final text = useListenableSelector(controller, () => controller.text);适合大对象里只关心单个字段的情况。
useValueNotifier
useValueNotifier 用来创建并自动销毁一个 ValueNotifier<T>。
dart
final counter = useValueNotifier(0);如果你想明确拿到一个自己维护的 ValueNotifier 对象,而不是像 useState 那样直接把它当本地状态用,这个 hook 更合适。
useValueListenable
useValueListenable 用来订阅一个 ValueListenable<T>,并直接返回当前值。
dart
class SearchField extends HookWidget {
const SearchField({super.key});
@override
Widget build(BuildContext context) {
final controller = useTextEditingController();
final value = useValueListenable(controller);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(controller: controller),
Text('当前输入:${value.text}'),
],
);
}
}常见场景:
- 监听
TextEditingController - 监听
ValueNotifier - 希望直接拿值,而不是自己注册监听器
useOnListenableChange
useOnListenableChange 用来给一个 Listenable 注册监听回调,并在不需要时自动移除。
dart
useOnListenableChange(
controller,
() => debugPrint(controller.text),
);如果你要做副作用,而不是直接让 UI 跟着重建,这个 hook 会更贴近需求。
Misc hooks
这一组 hook 数量最多,很多都是给 Flutter 控制器或系统状态提供的快捷封装。
useReducer
useReducer 是 useState 的替代方案,更适合复杂状态。
dart
enum CounterAction { increment, decrement, reset }
int counterReducer(int state, CounterAction action) {
switch (action) {
case CounterAction.increment:
return state + 1;
case CounterAction.decrement:
return state - 1;
case CounterAction.reset:
return 0;
}
}
class CounterPage extends HookWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context) {
final store = useReducer<int, CounterAction>(
counterReducer,
initialState: 0,
initialAction: CounterAction.reset,
);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('count: ${store.state}'),
Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: () => store.dispatch(CounterAction.decrement),
icon: const Icon(Icons.remove),
),
IconButton(
onPressed: () => store.dispatch(CounterAction.increment),
icon: const Icon(Icons.add),
),
],
),
],
);
}
}常见场景:
- 一个状态有多个操作入口
- 想把“状态变化规则”集中到 reducer
- 状态切换逻辑已经比较像有限状态机
usePrevious
usePrevious 用来获取上一次传入的值。
dart
final previousKeyword = usePrevious(keyword);适合做前后值对比、动画切换判断、埋点变化分析。
useTextEditingController
useTextEditingController 用来创建并自动销毁 TextEditingController。
dart
class LoginPage extends HookWidget {
const LoginPage({super.key});
@override
Widget build(BuildContext context) {
final controller = useTextEditingController(text: 'hello');
return TextField(controller: controller);
}
}常见场景:
- 文本输入框
- 搜索框
- 表单默认值填充
注意点:
- 初始
text或value只在首次创建时生效 - 如果外部值变化后你也想同步更新到输入框,要结合
useEffect
useFocusNode
useFocusNode 用来创建并自动销毁 FocusNode。
dart
final focusNode = useFocusNode();适合输入框聚焦控制、焦点切换、提交后自动聚焦下一个字段。
useTabController
useTabController 用来创建 TabController,并自动处理销毁。
dart
class TabPage extends HookWidget {
const TabPage({super.key});
@override
Widget build(BuildContext context) {
final controller = useTabController(initialLength: 3);
return Column(
children: [
TabBar(
controller: controller,
tabs: const [
Tab(text: 'A'),
Tab(text: 'B'),
Tab(text: 'C'),
],
),
Expanded(
child: TabBarView(
controller: controller,
children: const [
Center(child: Text('A')),
Center(child: Text('B')),
Center(child: Text('C')),
],
),
),
],
);
}
}常见场景:
TabBar/TabBarView- 页面内多标签切换
注意点:
initialLength是必填参数- 不传
vsync时,底层也会自动处理
useScrollController
useScrollController 用来创建并自动销毁 ScrollController。
dart
class ListPage extends HookWidget {
const ListPage({super.key});
@override
Widget build(BuildContext context) {
final controller = useScrollController();
return ListView.builder(
controller: controller,
itemCount: 50,
itemBuilder: (context, index) {
return ListTile(title: Text('item $index'));
},
);
}
}常见场景:
- 列表滚动定位
- 监听是否滚到底
- 回到顶部按钮
usePageController
usePageController 用来创建并自动销毁 PageController。
dart
final controller = usePageController();适合 PageView、引导页、横向翻页场景。
useFixedExtentScrollController
useFixedExtentScrollController 用来创建并自动销毁 FixedExtentScrollController。
dart
final controller = useFixedExtentScrollController();适合 ListWheelScrollView 这类固定项高滚动组件。
useAppLifecycleState
useAppLifecycleState 用来获取当前应用生命周期状态,并在状态变化时触发重建。
dart
final appLifecycleState = useAppLifecycleState();适合页面上直接展示或判断前后台状态。
useOnAppLifecycleStateChange
useOnAppLifecycleStateChange 用来监听应用生命周期变化并执行回调。
dart
useOnAppLifecycleStateChange((state) {
debugPrint('app state: $state');
});如果你只是想在切后台时暂停任务、回前台时恢复任务,这个 hook 比 useAppLifecycleState 更直接。
useTransformationController
useTransformationController 用来创建并自动销毁 TransformationController。
dart
final controller = useTransformationController();适合 InteractiveViewer 的缩放和平移控制。
useIsMounted
useIsMounted 提供一个和 State.mounted 类似的能力。
dart
final isMounted = useIsMounted();适合异步回调完成后,先判断当前 Widget 是否还活着,再决定是否继续操作。
useAutomaticKeepAlive
useAutomaticKeepAlive 是 AutomaticKeepAlive 的 hooks 版本。
dart
useAutomaticKeepAlive(wantKeepAlive: true);适合 TabBarView、分页内容、列表中的复杂子项希望保留状态的场景。
useOnPlatformBrightnessChange
useOnPlatformBrightnessChange 用来监听系统明暗主题变化。
dart
useOnPlatformBrightnessChange((brightness) {
debugPrint('brightness: $brightness');
});适合跟随系统主题、埋点记录或触发主题同步逻辑。
useSearchController
useSearchController 用来创建并自动销毁 SearchController。
dart
final controller = useSearchController();适合 Material 搜索栏或搜索页。
useWidgetStatesController
useWidgetStatesController 用来创建并自动销毁 WidgetStatesController。
dart
final controller = useWidgetStatesController();适合需要手动控制 Widget 状态集合的场景,例如选中、悬停、按压状态。
useExpansibleController
useExpansibleController 用来创建 ExpansibleController。
dart
final controller = useExpansibleController();适合控制可展开组件的展开和收起。
useDebounced
useDebounced 用来返回某个值的防抖版本。
dart
final debouncedKeyword = useDebounced(keyword, const Duration(milliseconds: 300));适合搜索输入、筛选条件、频繁变动参数的延迟提交。
useDraggableScrollableController
useDraggableScrollableController 用来创建 DraggableScrollableController。
dart
final controller = useDraggableScrollableController();适合底部可拖拽面板、抽屉式内容区。
useCarouselController
useCarouselController 用来创建并自动销毁 CarouselController。
dart
final controller = useCarouselController();适合新版轮播组件的页切换控制。
useTreeSliverController
useTreeSliverController 用来创建 TreeSliverController。
dart
final controller = useTreeSliverController();适合树形 Sliver 结构的数据展开、收起和状态控制。
useOverlayPortalController
useOverlayPortalController 用来创建并管理 OverlayPortalController。
dart
final controller = useOverlayPortalController();适合控制覆盖层显隐,比如气泡层、提示层、浮动菜单。
useSnapshotController
useSnapshotController 用来创建并管理 SnapshotController。
dart
final controller = useSnapshotController();适合依赖 Flutter 新控制器体系的快照、预览或拍摄相关场景。
useCupertinoController
useCupertinoController 用来创建并管理 CupertinoController。
dart
final controller = useCupertinoController();适合对应的 Cupertino 风格控件控制场景。
使用建议
如果你刚开始接触 Flutter Hooks,可以按下面的顺序理解:
- 先掌握
HookWidget和 hooks 调用规则 - 先熟悉
useState、useEffect、useMemoized - 再补
useFuture、useStream、useAnimationController - 然后掌握输入和滚动相关控制器 hooks
- 最后再看那些偏平台、控制器和新组件配套的 hooks
日常开发里,最常用的判断方式通常是:
- 只是一个简单本地状态,用
useState - 需要副作用和清理,用
useEffect - 需要缓存实例,用
useMemoized - 需要管理控制器对象,优先看有没有对应的
useXxxController
另外要注意,官方文档中的部分 hooks 对应的是 Flutter 新版本里的控制器类型。如果你当前 SDK 中没有某个类,通常不是文章写错,而是你的 Flutter 版本还没覆盖到那个 API。
