深色模式
Flutter Hooks
介绍
把 React hooks 搬到 Flutter 中,也就是 Flutter Hooks。
使用 Hooks,可以减少很多重复的样板代码。
安装
执行:
sh
flutter pub add flutter_hooks
或者,在pubspec.yaml
中:
yaml
dependencies:
...
flutter_hooks: ^0.18.5+1
使用 Hooks 的规则
hook底层是根据hook数组的index来维护的,所以使用hook时,要遵守一些规则:
继承
HookWidget
使用hook时,组件要继承
HookWidget
,而不是StatelessWidget
或StatefulWidget
。当使用
hooks_riverpod
库时,则组件可以继承HookConsumerWidget
或StatefulHookConsumerWidget
,因为这2个类,都继承HookWidget
。hook函数都以
use
开头hook函数不可以在条件语句中使用
可以:
dartWidget build(BuildContext context) { useMyHook(); // .... }
不可以:
dartWidget build(BuildContext context) { if (condition) { useMyHook(); } // .... }
hook 在 hot-reload 时的行为
假设初始状态为:
dartuseA(); useB(0); useC();
代码改为:
dartuseA(); useB(42); useC();
HookA、HookC保持状态,HookB会热重载。
代码改为:
dartuseA(); useC();
HookA保持状态,HookC重新加载。
可以自定义hook
有利于抽离逻辑,关注点分离。
比如:录音、播放,需要处理初始化、配置、状态判断、开始、暂停、停止等控制,可以抽取为单独的hook。
例,自定义一个hook名为
useAudioPlayer
,然后使用它:dart@override Widget build(BuildContext context, WidgetRef ref) { final ( playStatus, playDuration, startPlay, pausePlay, resumePlay, stopPlay, ) = useAudioPlayer(filePath); }
dartenum AudioStatus { idle, playing, paused, error, } /// 抽离播放逻辑 /// 返回播放状态、播放进度、播放控制方法 ( ValueNotifier<AudioStatus> status, ValueNotifier<int> duration, Future<void> Function() start, Future<void> Function() pause, Future<void> Function() resume, Future<void> Function() stop, ) useAudioPlayer(String recordPath) { final duration = useState(0); final status = useState(AudioStatus.idle); final player = useMemoized(() => AudioPlayer()); useEffect(() { // 监听播放完成 final completeSubscription = player.onPlayerComplete.listen((_) { status.value = AudioStatus.idle; duration.value = 0; }); // 监听播放进度 final positionSubscription = player.onPositionChanged.listen((position) { duration.value = position.inSeconds; }); return () { completeSubscription.cancel(); positionSubscription.cancel(); player.dispose(); }; }, []); Future<void> start() async { if (recordPath.isEmpty) { status.value = AudioStatus.error; return; } try { await player.play(DeviceFileSource(recordPath)); status.value = AudioStatus.playing; } catch (e) { status.value = AudioStatus.error; } } pause() async { await player.pause(); status.value = AudioStatus.paused; } resume() async { await player.resume(); status.value = AudioStatus.playing; } Future<void> stop() async { await player.stop(); status.value = AudioStatus.idle; duration.value = 0; } return ( status, duration, start, pause, resume, stop, ); }
Hooks API
useEffect
此函数是hook的核心,这里重点记录一下。
函数定义:
dart
void useEffect(Dispose? Function() effect, [List<Object?>? keys]) {
use(_EffectHook(effect, keys));
}
typedef Dispose = void Function();
函数解释:
useEffect
: hook函数名effect
: 副作用函数Dispose?
: 清理函数,它是effect的返回值,是一个函数类型,可空,它的作用是清除副作用。keys
: 依赖数组
规则:
- 如果
keys
为空,useEffect
在每次build
时,都会同步调用。 - 如果
keys
不为空,useEffect
只在首次build
时,以及keys
数组中任意值发生变化时调用。 effect
被useEffect
同步调用。effect
可以返回一个清理函数,此函数在2种情况下被调用effect
被再次调用时- 组件dispose
注意:
effect
不可以是async
函数dart// 错误用法 useEffect(() async { return null; }, []);
清理函数调用时机
当组件dispose时,清理函数会被调用,这没有争议。
当
effect
再次触发时,清理函数会被调用,它的作用是清除上一次effect
的副作用,但是从时序上来看,清理函数会在下次effect
之后才调用,而不是在它之前调用。所以,不能依赖时序来保证清理逻辑的正确。
清理函数是一个闭包,它会捕获外部的变量,在使用的时候,尽量不要捕获
effect
函数外层的变量,否则,由于它的调用时序,容易逻辑出问题,比如:刚刚订阅一个流,然后马上就把它释放了。
useState
dart
ValueNotifier<T> useState<T>(T initialData) {
return use(_StateHook(initialData: initialData));
}
initialData
:状态的初始值- 返回一个
ValueNotifier
对象,每当ValueNotifier.value
发生变化,会将所属的Widget
标记为需要重要重新build
。 Widget
每次build
时,useState()
返回的ValueNotifier
对象,是不同的对象哦,但是ValueNotifier.value
的值会保持。
useMemoized
dart
T useMemoized<T>(
T Function() valueBuilder, [
List<Object?> keys = const <Object>[],
]) {
return use(
_MemoizedHook(
valueBuilder,
keys: keys,
),
);
}
- 立即执行
valueBuilder
,返回一个值 - 在组件rebuild时,
valueBuilder
不会重复执行,而是返回上次缓存的值 - 如果
keys
发生变化,在rebuild时,valueBuilder
才会重复执行
当组件销毁时,缓存值不再有效。
useRef
dart
ObjectRef<T> useRef<T>(T initialValue) {
return useMemoized(() => ObjectRef<T>(initialValue));
}
class ObjectRef<T> {
ObjectRef(this.value);
T value;
}
返回一个值的包装引用,值改变时,不会触发rebuild。这一点与useState
相对。
useCallback
dart
T useCallback<T extends Function>(
T callback, [
List<Object?> keys = const <Object>[],
]) {
return useMemoized(() => callback, keys);
}
- 缓存一个函数,它是
useMemoized
的语法糖
代码对比:
dart
final cachedFunction = useCallback(() {
print('doSomething');
}, [key]);
dart
final cachedFunction = useMemoized(() => () {
print('doSomething');
}, [key]);
useContext
dart
BuildContext useContext() {
assert(
HookElement._currentHookElement != null,
'`useContext` can only be called from the build method of HookWidget',
);
return HookElement._currentHookElement!;
}
- 返回当前
HookWidget
的BuildContext
useValueChanged
dart
R? useValueChanged<T, R>(
T value,
R? Function(T oldValue, R? oldResult) valueChange,
) {
return use(_ValueChangedHook(value, valueChange));
}
- 观察一个值,每当值改变时,触发回调。
valueChange
函数不会在定义时立即调用valueChange
函数可以返回一个值useValueChanged
勾子的返回值,是最近一次valueChange
的返回值。
useReducer
(这个hook没什么卵用。)
dart
Store<State, Action> useReducer<State, Action>(
Reducer<State, Action> reducer, {
required State initialState,
required Action initialAction,
}) {
return use(
_ReducerHook(
reducer,
initialAction: initialAction,
initialState: initialState,
),
);
}
示例:
dart
// 定义状态
class CounterState {
final int count;
CounterState(this.count);
}
// 定义动作
enum CounterAction { increment, decrement }
dart
CounterState counterReducer(CounterState state, CounterAction action) {
switch (action) {
case CounterAction.increment:
return CounterState(state.count + 1);
case CounterAction.decrement:
return CounterState(state.count - 1);
default:
return state;
}
}
dart
class CounterWidget extends HookWidget {
@override
Widget build(BuildContext context) {
final counter = useReducer(counterReducer, initialState: CounterState(0));
return Column(
children: <Widget>[
Text('Count: ${counter.state.count}'),
ElevatedButton(
onPressed: () => counter.dispatch(CounterAction.increment),
child: Text('Increment'),
),
ElevatedButton(
onPressed: () => counter.dispatch(CounterAction.decrement),
child: Text('Decrement'),
),
],
);
}
}