冗余代码是编程之大忌,如何优雅的减少冗余代码,增加摸鱼时长是每个程序员都要追求的目标之一。今天给大家介绍一款Flutter开发摸鱼小工具--Flutter Hooks,官方地址:pub.flutter-io.cn/packages/fl…
我们从一个入门案例说起,点击按钮增加计数,因为需要更新UI,使用StatefulWidget
class Counter extends StatefulWidget {
const Counter({Key? key}) : super(key: key);
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int count = 0;
@override
Widget build(BuildContext context) {
print("rebuild 1");
return Scaffold(
appBar: AppBar(
title: const Text('useState example'),
),
body: GestureDetector(
onTap: () {
// 使页面重建
setState(() {
count++;
});
},
child: Center(
child: Text("tap ${count} times"),
),
));
}
}
代码的重点就是setState方法,当我们需要更新页面的时候,调用此方法会引起系统调用build方法重建此Widget树,那有没有什么办法,当count发生变动时,可以自动更新Widget。其实是有的,Flutter Hooks中的useState就提供了一个这样的实现。
Hook的使用
- 引入版本 flutter_hooks: ^0.12.0
- 继承HookWidget、StatefulHookWidget
- 使用useState方法即可
class Counter extends HookWidget {
@override
Widget build(BuildContext context) {
final counter = useState(0);
return Scaffold(
appBar: AppBar(
title: const Text('useState example'),
),
body: Center(
child: Text('Button tapped ${counter.value} times'),
),
floatingActionButton: FloatingActionButton(
// 当counter中的值发生变化,会通知组件更新,不需要我们手动调用setState
onPressed:() => counter.value++,
child: const Icon(Icons.add),
),
);
}
}
上面使用Hook技术,运行后有同样的效果,但有几点比较奇怪
- 没有State代码,HookWidget是继承StatelessWidget,怎么更新页面的
- useState方法是重复调用的,但并没有重复初始化,这是为什么。
在继续分析之前,先看一下还有哪些用法,比如你想提升一下渲染效率,不想整个Counter重建,可以把body分离出去,这样每次点击只会重建Body组件
class Counter extends StatelessWidget {
const Counter3({super.key});
@override
Widget build(BuildContext context) {
print("Counter rebuild 1");
return Scaffold(
appBar: AppBar(
title: const Text('useState example'),
),
body: Body());
}
}
class Body extends HookWidget{
const Body({super.key});
@override
Widget build(BuildContext context) {
print("Body rebuild");
ValueNotifier<int> count = useState(0);
return GestureDetector(
onTap: () {
count.value++;
},
child: Center(
child: Text("tap ${count.value} times"),
),
);
}
}
只是这样写还挺费事,Hook提供了HookBuilder组件,继承自HookWidget,能实现同样效果
class Counter extends StatelessWidget {
const Counter({super.key});
@override
Widget build(BuildContext context) {
print("Counter rebuild 1");
return Scaffold(
appBar: AppBar(
title: const Text('useState example'),
),
body: HookBuilder(builder: (context) {
print("Counter rebuild 2");
ValueNotifier<int> count = useState(0);
return GestureDetector(
onTap: () {
count.value++;
},
child: Center(
child: Text("tap ${count.value} times"),
),
);
}));
}
}
Hook组件关系图
关于Hook的使用不是本文介绍的重点,我们重点分析其实现原理。在上面案例中,使用Hook的地方都涉及到了HookWidget,我们从这组件入手,了解Hook的整体架构。如下图,Hook提供了两个组件:HookWidget或StatefulHookWidget,其实它们就是直接继承了StatelessWidget或StatefulWidget
abstract class HookWidget extends StatelessWidget {
/// Initializes [key] for subclasses.
const HookWidget({Key? key}) : super(key: key);
@override
_StatelessHookElement createElement() => _StatelessHookElement(this);
}
abstract class StatefulHookWidget extends StatefulWidget {
/// Initializes [key] for subclasses.
const StatefulHookWidget({Key? key}) : super(key: key);
@override
_StatefulHookElement createElement() => _StatefulHookElement(this);
}
继承的重点是复写了createElement方法,分别返回了_StatelessHookElement和_StatefulHookElement对象,而这两个对象也分别继承了StatelessElement和StatefulElement,还有一个HookElement,其实啥都没干
class _StatelessHookElement extends StatelessElement with HookElement {
_StatelessHookElement(HookWidget hooks) : super(hooks);
}
class _StatefulHookElement extends StatefulElement with HookElement {
_StatefulHookElement(StatefulHookWidget hooks) : super(hooks);
}
这里的with HookElement是一个mixin技术,如果不用这个mixin,应该在_StatelessHookElement和_StatefulHookElement中对父类方法进行复写,但两个类的复写代码是雷同的,而mixin技术就是为了在多个类中复用代码而生,所以这两个类都混入继承了HookElement,完成了对父类的复写,那么HookElement就是实现的重点。我们继承HookWidget或StatefulHookWidget就是为了使用HookElement
从下面代码看,HookElement使用on关键字限定了ComponentElement,它有继承作用,HookElement可以使用super访问ComponentElement方法,也有限定作用,就是要混入这类,必须实现或继承了ComponentElement,_StatefulHookElement和_StatelessHookElement都是符合条件的。
Hook初始化
Counter组件继承了HookWidget就引入了HookElement,当应用启动,控件树开始构建,对于我们的组件来说,首先调用到的是HookElement的build方法,其中的super.build()调用的是ComponentElement的build方法,
mixin HookElement on ComponentElement {
...
_Entry<HookState> _currentHookState;
final LinkedList<_Entry<HookState>> _hooks = LinkedList();
...
@override
Widget build() {
...
_currentHookState = _hooks.isEmpty ? null : _hooks.first;
// 静态方法中保持了当前实例,useState方法中要用到
HookElement._currentHookElement = this;
// 调用父类方法,最后是调用Counter组件的build方法
_buildCache = super.build();
...
return _buildCache;
}
}
ComponentElement的build方法被StatelessElement复写,也就调用到了Counter的build方法
class StatelessElement extends ComponentElement {
/// Creates an element that uses the given widget as its configuration.
StatelessElement(StatelessWidget super.widget);
@override
Widget build() => (widget as StatelessWidget).build(this);
...
}
在Counter组件的build方法中使用了useState方法
ValueNotifier<int> counter = useState(0);
useState使用的是_StateHook,如下图,继承自Hook
ValueNotifier<T> useState<T>(T initialData) {
return use(_StateHook(initialData: initialData));
}
除图中列出的一些,还有_TextEditingControllerHook等内置的Hook,它们的重点在于复写createState方法,返回HookState的子类,如下图,我们使用的_StateHook返回的就是_StateHookState
HookState有两个重要的成员变量HookElement和Hook,如下图,HookElement有一个HookState链表,说明它是支持多个Hook同时使用的,这个对我们使用是有限制的
到此梳理一下Hook组件的关系图,大致如下,菱形箭头表示组合关系,三角形是继承关系
use方法最后调用的是HookElement._currentHookElement对象,它是之前HookElement build方法使用当前hookElement对象初始化的静态成员变量,就是为了在这里方便调用
static R use<R>(Hook<R> hook) {
return HookElement._currentHookElement!._use(hook);
}
这样我们就走到了当前HookElement的use方法
R _use<R>(Hook<R> hook) {
/// 第一次build时,_currentHookState为空
if (_currentHookState == null) {
_appendHook(hook);
} else if (hook.runtimeType != _currentHookState!.value.hook.runtimeType) {
...
} else if (hook != _currentHookState!.value.hook) {
...
}
// result是暴露给用户使用的,对于useState来说,返回的就是ValueNofity
final result = _currentHookState!.value.build(this) as R;
...
// 指向链表的下一个节点,如果没有就是空,这样后面接着调用use方法,就会继续走_appendHook
_currentHookState = _currentHookState!.next;
return result;
}
组件第一次build重点看_appendHook,这里使用hook创建了HookState,并初始化了其hookElement和hook成员变量,然后把HookState封装成Entry添加到链表结尾
void _appendHook<R>(Hook<R> hook) {
final result = _createHookState<R>(hook);
_currentHookState = _Entry(result);
// 将新建的HookState添加到链表尾部
_hooks.add(_currentHookState!);
}
HookState<R, Hook<R>> _createHookState<R>(Hook<R> hook) {
// 这里hook创建了HookState并完成一些初始化,
// 这样每个HookState都持有HookElement和Hook的引用,而HookElement通过链表持用所有HookState引用
final state = hook.createState()
.._element = this
.._hook = hook
..initHook();
return state;
}
上面hook.createState()返回的就是_StateHookState,其build方法返回给用户的是ValueNotifier,它才是持用数据的地方,当我们调用counter.value++,其持用状态值发生变化,_listener方法就会被调用,然后调用setState方法。
class _StateHookState<T> extends HookState<ValueNotifier<T>, _StateHook<T>> {
late final _state = ValueNotifier<T>(hook.initialData)
..addListener(_listener);
@override
void dispose() {
_state.dispose();
}
@override
ValueNotifier<T> build(BuildContext context) => _state;
void _listener() {
setState(() {});
}
}
这里的setState方法继承自HookState,HookState是持用HookElement引用的,可以调用element的markNeedsBuild方法引起组件重建,事实上,StatefulWidget的setState也是这么干的,element才是更新组件的关键而不是Widget。
void setState(VoidCallback fn) {
fn();
_element!
.._isOptionalRebuild = false
..markNeedsBuild();
}
在上面还提出一个问题,为什么Counter组件重复build,不会将useState里面的值重新初始化为0。首先当我们调用hookElement的markNeedsBuild方法将自己标记为dirty后,系统重建控件树,hookElement的build方法先被系统调用,此时链表已不为空了_currentHookState会被初始化
mixin HookElement on ComponentElement {
@override
Widget build() {
// 先初始化当前hookState
_currentHookState = _hooks.isEmpty ? null : _hooks.first;
...
return _buildCache;
}
}
然后是useState方法被调用,
- _currentHookState在上面初始化了,不为空;
- hook是新的但runtimeType类型没变;
- hook是新的,但keys参数使用的是默认值,是一样的,走的是更新Hook逻辑,HookState还是之前的,所以返回的result还是之前的
R _use<R>(Hook<R> hook) {
/// 再build时,_currentHookState不为空
if (_currentHookState == null) {
_appendHook(hook);
} else if (hook.runtimeType != _currentHookState!.value.hook.runtimeType) {
// 抛出异常...
} else if (hook != _currentHookState!.value.hook) {
final previousHook = _currentHookState!.value.hook;
// 通过比较hook的keys决定是否保留HookState
if (Hook.shouldPreserveState(previousHook, hook)) {
// 如果保留就更新一下hook
_currentHookState!.value
.._hook = hook
..didUpdateHook(previousHook);
} else {
// 如果不保留就创建新的HookState替换,并回收之前的
_needDispose ??= LinkedList();
_needDispose!.add(_Entry(_currentHookState!.value));
_currentHookState!.value = _createHookState<R>(hook);
}
}
// result是暴露给用户使用的,对于useState来说,返回的就是ValueNofity
final result = _currentHookState!.value.build(this) as R;
...
// 初始化下一个
_currentHookState = _currentHookState!.next;
return result;
}
为什么上面use方法中要判断runtimeType呢,上面说过,HookElement是支持多个Hook的,我们在HookWidget中可以像下面这样使用多个,HookElement中的链表是按顺序存取HookState,一 一对应的
class Counter extends HookWidget {
@override
Widget build(BuildContext context) {
final counter = useState(0);
final animationController = useAnimationController();
final textEditControl = useTextEditingController();
return ...;
}
}
但如果你使用了条件语句,就可能破环了这种对应关系,导致hook.runtimeType前后不一致,所以使用Hook的前提就是不要将其放在条件语句中
class Counter extends HookWidget {
@override
Widget build(BuildContext context) {
final counter = useState(0);
if(condition) {final animationController = useAnimationController ();}
final textEditControl = useTextEditingController();
return ...;
}
}
到此,我们以setState为例,分析这个Hook的工作原理就基本结束了,最后总结一下大致的流程