看了Resocoder和Robert Brunhage的两个视频, 这篇总结一下其中的内容(我其实还没实践过).
本文主要参考自:
- pub package
- Video by Robert Brunhage
- Tutorial by Resocoder
The problem
Flutter hooks想解决的问题是StatefulWidget的一些常见pattern太复杂, 减少了代码的可读性("readability").
一个典型例子就是animation controller:
class MyPage extends StatefulWidget{
@override
_MyPageState createState() => _MyPageState();
}
class _MyPageState extends State<MyPage>
with SingleTickerProviderStateMixin {
AnimationController _animController;
@override
void initState() {
super.initState();
_animController = AnimationController(
vsync: this,
duration: kThemeAnimationDuration,
value: 1, // initially visible
);
}
@override
Widget build(BuildContext context) {
// use _animController here
}
@override
void dispose() {
_animController.dispose();
super.dispose();
}
}
这种pattern在vanilla flutter里经常被使用, 但是对同一个controller在好几个地方操作(initiState()/setState()/dispose()), 真的不好读.
Flutter hooks就可以把这种重复的pattern抽离出来, 把StatefulWidget变成类似StatelessWidget的HookWidget, 简化代码, 大大提高可读性.
(btw 这个包的作者是Rémi Rousselet大神, 质量有保障👌)
使用现成的Hooks
对于常用的pattern有现成的hooks可以直接套用. 比如上面的animation controller例子, 用flutter hooks可以写成这样:
class MyPage extends HookWidget { //! StatelessWidget => HookWidget
@override
Widget build(BuildContext context) {
final animController = useAnimationController(
duration: kThemeAnimationDuration,
initialValue: 1,
);
// use _animController here
}
}
⚡️Flutter hooks自带了不少现成的hooks, 有的面向Animation, 有的面向Stream等等, 有时间可以好好看一看...
自定义Hook
cf. Resocoder的教程
如果现成的hook不满足条件, 其实自己实现起来也非常容易, 基本就是把原来StatefulWidget/State<Foo>的内容变成Hook<R>/HookState<R,Foo>. 其中Foo是我们自定义的类名, R则是state的类型. 比如对于之前的例子: Foo=MyPage, R=AnimationController.
这里列一下二者的区别, 基本是一一对应的, 所以迁移起来不难:
StatefulWidget |
Hook<R> |
|---|---|
class Foo extends StatefulWidget |
class Foo extends Hook<R> |
createState() => State<Foo> |
createState() => HookState<R> |
State<Foo> |
HookState<R, Foo> |
|---|---|
R _data; |
R _data; |
initState() {_data=...} |
initHook() {_data=...} |
build(ctx) => Widget |
build(ctx) => R |
dispose() |
dispose() |
widget.xxx |
hook.xxx |
当自定义的Hook/HookState写好以后, 根据Flutter hooks的惯例, 需要写一个useXxHooks的函数, 返回类型是R:
R useMyHooks(...) {
return Hook.use(_FooHook());
}
useState/useEffect
cf. Robert Brunhage的视频
对于不追求逻辑复用, 只想去掉initState()/dispose()的场景, 可以考虑直接把一个StatefulWidget变成HookWiget. Flutter hooks提供了useState和useEffect, 让我们可以直接在HookWiget.build()函数里面创建/修改状态.
创建状态: 用useState(R initialVal).\
注意返回值的是一个ValueNotifier<R>, 获得包含的值需要.value.
修改状态: useEffect(), 有两个参数:
- 第一个参数(
effect())是一个函数Dispose? Function()- 可以把修改状态的内容(比如原先
setState的内容)放在这里 effect可以返回另一个函数(Dispose?), 这个函数会_在effect()下次一被调用或者当widget dispose的时候运行_.
- 可以把修改状态的内容(比如原先
- 第二个可选参数
keys是一个list, 控制effect()何时被调用:- 如果为null(默认), 则_每次
build()_ 都会调用effect() - 如果非null, 则只在_第一次
build(), 以及当任何keys元素改变时_才调用effect()\ 注意:keys可以是空list, 但非null, 比如keys=[], 此时effect只运行一次.
- 如果为null(默认), 则_每次
这种方法可以把一个StatefulWidget简化为类似StatelessWidget的HookWidget, 简化代码. 例如用useState/useEffect写一个简单计时器, 代码相比vanilla flutter要简洁许多:
class MyTimerPage extends HookWidget {
@override
Widget build(BuildContext context) {
final _numberNotifier = useState(0); //! Create state
useEffect( //! Change state
/*effect=*/() {
//! ~= initState()
final timer = Timer.periodic(
Duration(seconds: 1),
//! ~= setState()
(time) => _numberNotifier.value = time.tick,
);
return timer.cancel; //! ~= dispose()
},
/*keys=*/const [], //! => effect() is called only once
);
return Scaffold(
body: Center(
child: Text(_numberNotifier.value.toString()),
),
);
}
}
当然这样写的缺点就是逻辑无法复用, 要复用逻辑的话可以参考上一节"自定义Hook":
-
把useState/useEffect的内容放进一个
_MyTimerHook extends Hook<int> -
定义
int useMyTimer()函数:dart int useMyTimer() { return Hook.use(_MyTimerHook()); } -
在
HookWidget里使用useMyTimerHook:dart class MyTimerPage extends HookWidget { @override Widget build(BuildContext context) { final number = useMyTimer(); return Text(number.toString()); } }
最佳实践
pub上列出了几条最佳实践:
所有hook的函数都以'use'开头:
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();
// ....
}
不要在分支条件里使用hook:
Widget build(BuildContext context) {
useMyHook(); //! Good: DO call hooks unconditionally
// ....
}
Widget build(BuildContext context) {
if (condition) {
useMyHook(); //! BAD: DON'T wrap use into a condition
}
// ....
}
Disqus 留言