前言
Hooks,直译过来就是"钩子",是前端React框架加入的特性,用来分离状态逻辑和视图逻辑。现在这个特性并不只局限在于React框架中,其它前端框架也在借鉴。同样的,我们也可以在Flutter中使用Hooks。Hooks对于从事Native开发的开发者可能比较陌生。但Flutter的一大优势就是综合了H5,Native等开发平台的优势,对Native开发者和对H5开发者都比较友好。所以通过这篇文章来介绍Hooks,希望大家能对这一特性有所了解。
为什么引入Hooks
我们都知道在FLutter开发中的一大痛点就是业务逻辑和视图逻辑的耦合。这一痛点也是前端各个框架都有的痛点。所以大家就像出来各种办法来分离业务逻辑和视图逻辑,有MVP,MVVM,React中的Mixin,高阶组件(HOC),直到Hooks。Flutter中大家可能对Mixin比较熟悉,我之前写过一篇文章介绍使用Mixin这种方式来分离业务逻辑和视图逻辑。
Mixin的方式在实践中也会遇到一些限制:
- Mixin之间可能会互相依赖。
- Mixin之间可能存在冲突。
因此我们引入Hooks来看看能不能避免Mixin的这些限制。
Flutter Hooks使用
引入Hooks需要在pubspec.yaml
加入以下内容
flutter_hooks: ^0.12.0
Hooks函数一般以use
开头,格式为useXXX
。React定义了一些常用的Hooks函数,如useState
,useEffect
等等。
useState
useState我们可能会比较常用,用来获取当前Widget所需要的状态。
我们以Flutter的计数器例子来介绍一下如何使用Hooks,代码如下:
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
void main() {
runApp(MaterialApp(
home: HooksExample(),
));
}
class HooksExample 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(
onPressed:() => counter.value++,
child: const Icon(Icons.add),
),
);
}
}
我们来看一下使用Hooks的计数器和原生的计数器例子源码有什么样的区别。
- 首先原生的计数器因为要保存
counter
这个状态,所以使用的是一个StatefulWidget
。counter
保存在对应的State
中。而使用Hooks改造过的计数器却没有使用StatefulWidget
,而是继承自HookWidget
, 它其实是一个StatelessWidget
。
class HooksExample extends HookWidget {
- 其次我们看到计数器的状态
counter
是通过调用函数useState()
获取到的。入参0代表初始值。
final counter = useState(0);
- 最后就是在点击事件的处理上,我们只是把计数器数值+1。并没有去调用
setState()
,计数器就会自动刷新。
onPressed:() => counter.value++
可见相比于原生Flutter的模式,同样做到了将业务逻辑和视图逻辑分离。不需要再使用StatefulWidget
,就可以做到对状态的访问和维护。
我们也可以在同一个Widget
下引入多个Hooks:
final counter = useState(0);
final name = useState('张三');
final counter2 = useState(100);
这里要特别注意的一点是,使用Hooks的时候不可以在条件语句中调用useXXX
,类似以下这样的代码要绝对避免。
if(condition) {
useMyHook();
}
熟悉Hooks的同学可能会知道这是为什么。具体原因我会在下面的Flutter Hooks原理小结中做以说明。
useMemoized
当你使用了BLoC或者MobX,可能需要有一个时机来创建对应的store。这时你可以让useMemoized
来为你完成这项工作。
class MyWidget extends HookWidget {
@override
Widget build(BuildContext context) {
final store = useMemoized(() => MyStore());
return Scaffold(...);
}
}
useMemoized
的入参是个函数,这个函数会返回MySotre
实例。此函数在MyWidget
的生命周期内只会被调用一次,得到的MySotre
实例会被缓存起来,后续再次调用useMemoized
会得到这一缓存的实例。
useEffect
在首次创建MySotre
实例之后我们一般需要做一些初始化工作,例如开始加载数据之类。有时候或许在Widget
生命周期结束的时候做一些清理工作。这些事情则会由useEffect
这个Hook来做。
class MyWidget extends HookWidget {
@override
Widget build(BuildContext context) {
final store = useMemoized(() => MyStore());
useEffect((){
store.init();
return store.dispose;
},const []);
return Scaffold(...);
}
}
useEffect
的入参函数内可以做一些初始化的工作。如果需要在Widget
生命周期结束的时候做一些清理工作,可以返回一个负责清理的函数,比如代码里的store.dispose
。useEffect
的第二个入参是一个空数组。这样就保证了初始化和清理函数只会在Widget
生命周期开始和结束时各被调用一次。如果不传这个参数的话则会在每次build
的时候都会被调用。
其他Hooks
除了以上这些Hooks,flutter_hooks
还提供了一些可以节省我们代码量的Hooks。如useAnimationController
,提供AnimationController
直接用而不用去操心初始化以及释放资源的事情。还有useTabController
,useTextEditingController
等等,完整Hooks列表大家可以去flutter_hooks@github查看。
自定义Hooks
当以上Hooks不能满足需求时,我们也可以自定义Hooks。自定义Hooks有两种方式,一种是用函数来自定义自定义Hooks,如果需求比较复杂的话还可以用类来自定义Hooks。
Function
这种方式一般来讲就是用我们自定义的函数来包裹组合原生的Hooks。比如对前面的计数器那个例子。我们想在技术器增加的时候除了界面上有显示,还需要在日志里打出来。那么就可以这样来自定义一个Hook:
ValueNotifier<T> useLoggedState<T>(BuildContext context, [T initialData]) {
final result = useState<T>(initialData);
useValueChanged(result.value, (_, __) {
print(result.value);
});
return result;
}
Class
如果需求比较复杂,需要在Widget
的各个生命周期做处理,则可以用类的方式来自定义Hook。这里我们来自定义一个Hook,作用是作用是打印出Widget
存活的时长。我们知道Hooks都是以useXXX
作为名字的函数。所以我们先来给出这样的函数
Result useTimeAliveHook(BuildContext context) {
return use(const _TimeAlive());
}
然后就是对应的类:
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();
}
}
看起来是不是有一种很熟悉的感觉?这不就是一个StatefulWidget
嘛。对的,flutter_hooks其实就是借鉴了Flutter自身的一些机制来达到Hooks的目的。那些自带的useState
也都是这么写的。也就是看起来很复杂的需要StatefulWidget
来完成的工作现在简化为一个useXXX
的函数调用其实是因为flutter_hooks帮你把事情做了。至于这背后是怎样的一个机制,下一节我们通过源码来了解一下Flutter Hooks的原理。
Flutter Hooks原理
在了解Flutter Hooks原理之前我们要先提几个问题。在用Hooks改造计数器之后,就没有了StatefulWidget
。那么计数器的状态放在哪里了呢?在状态发生变化之后界面又是如何响应的呢?带着这些问题让我们来探索Flutter Hooks的世界
HookWidget
首先来看HookWidget
。
abstract class HookWidget extends StatelessWidget {
const HookWidget({Key key}) : super(key: key);
@override
_StatelessHookElement createElement() => _StatelessHookElement(this);
}
class _StatelessHookElement extends StatelessElement with HookElement {
_StatelessHookElement(HookWidget hooks) : super(hooks);
}
它继承自StatelessWidget
。并且重写了createElement
。其对应的element
是_StatelessHookElement
。而这个element
只是继承了StatelessElement
并且加上了HookElement
的mixin
。所以关键的东西应该都是在HookElement
里面。
HookElement
看一下HookElement
:
mixin HookElement on ComponentElement {
...
_Entry<HookState> _currentHookState;
final LinkedList<_Entry<HookState>> _hooks = LinkedList();
...
@override
Widget build() {
...
_currentHookState = _hooks.isEmpty ? null : _hooks.first;
HookElement._currentHookElement = this;
_buildCache = super.build();
return _buildCache;
}
}
HookElement
有一个链表,_hooks
保存着所有的HookState
。还有一个指针_currentHookState
指向当前的HookState
。我们看一下build
函数。在每次HookElement
做build
的时候都会把_currentHookState
指向_hooks
链表的第一个元素。然后才走Widget
的build
函数。也就是说,每次重建Widget
的时候都会重置_currentHookState
。记住这一点。
另一个问题。我们不是在讨论Hooks吗?那这里的HookState
和Hook
又是什么关系呢?
Hook
abstract class Hook<R> {
const Hook({this.keys});
@protected
HookState<R, Hook<R>> createState();
}
Hook
这个类就很简单了,而且看起来很像一个StatefulWidget
。那么对应的State
就是HookState
了。
HookState
abstract class HookState<R, T extends Hook<R>> {
@protected
BuildContext get context => _element;
HookElement _element;
T get hook => _hook;
T _hook;
@protected
void initHook() {}
@protected
void dispose() {}
@protected
R build(BuildContext context);
@protected
void didUpdateHook(T oldHook) {}
void reassemble() {}
/// Equivalent of [State.setState] for [HookState]
@protected
void setState(VoidCallback fn) {
fn();
_element
.._isOptionalRebuild = false
..markNeedsBuild();
}
}
简直和State
一毛一样。我们可以直接拿StatefulWidget
和State
的关系来理解Hook
和HookState
的联系了。有一点区别是State.build
返回值是个Widget
。而HookState.build
的返回值则是状态值。
另外,一个StatefulElement
只会持有一个State
。而HookElement
则可能持有多个HookState
,并且把这些HookState
都放在_hooks
这个链表里。如下图所示:
use
至此我们知道了引入Hooks以后那些状态都放在哪里。那么这些状态又是何时被添加,何时被使用的呢?这就要说说那些useXXX
函数了。从之前我们说的用类的方式来自定义Hook的时候了解到,每次调用useXXX
都会新建一个Hooks
实例。
Result useTimeAliveHook(BuildContext context) {
return use(const _TimeAlive());
}
虽然Hook每次都是新的,但是HookState
却还是原来那个。这个就参照StatefulWidget
每次都是新的但State
却不变来理解就是了。这个useXXX
最终会调用到HookElement._use
:
R _use<R>(Hook<R> hook) {
if (_currentHookState == null) {
_appendHook(hook);
} else if (hook.runtimeType != _currentHookState.value.hook.runtimeType) {
...
throw StateError('''
Type mismatch between hooks:
- previous hook: $previousHookType
- new hook: ${hook.runtimeType}
''');
}
} else if (hook != _currentHookState.value.hook) {
final previousHook = _currentHookState.value.hook;
if (Hook.shouldPreserveState(previousHook, hook)) {
_currentHookState.value
.._hook = hook
..didUpdateHook(previousHook);
} else {
_needDispose ??= LinkedList();
_needDispose.add(_Entry(_currentHookState.value));
_currentHookState.value = _createHookState<R>(hook);
}
}
final result = _currentHookState.value.build(this) as R;
_currentHookState = _currentHookState.next;
return result;
}
这个函数也是Hooks运行的核心,需要我们仔细去理解。
第一个分支,如果_currentHookState
为空,说明此时_hook
链表为空或者_currentHookState
指向的是链表末尾元素的下一个。换而言之,当前调用use
对应的HookState
还不在链表中,那么就调用_appendHook
来将其加入链表
void _appendHook<R>(Hook<R> hook) {
final result = _createHookState<R>(hook);
_currentHookState = _Entry(result);
_hooks.add(_currentHookState);
}
在这里我们可以看到_createHookState
被调用,生成的HookState
实例被加入链表。
第二个分支,如果新Hook的运行时类型与当前Hook的运行时类型不一样,此时会抛出异常。
第三个分支,如果新老Hook类型一致,实例不一样,那么就要看是否保留状态,如果保留的话就先更新Hook,然后调用HookState.didUpdateHook
。这个函数由其子类实现;如果不保留状态,那就调用_createHookState
重新获取一个状态实例把原来的给替换掉。一般来讲我们都是想保留状态的,这也是Flutter Hooks的默认行为,具体判断呢则是在函数Hook.shouldPreserveState
内:
static bool shouldPreserveState(Hook hook1, Hook hook2) {
final p1 = hook1.keys;
final p2 = hook2.keys;
if (p1 == p2) {
return true;
}
if ((p1 != p2 && (p1 == null || p2 == null)) || p1.length != p2.length) {
return false;
}
final i1 = p1.iterator;
final i2 = p2.iterator;
while (true) {
if (!i1.moveNext() || !i2.moveNext()) {
return true;
}
if (i1.current != i2.current) {
return false;
}
}
}
这个函数就是在比较两个新老Hooks的keys
。如果为空或者相等,那么就认为是要保留状态,否则不保留。
分支走完了最后就是通过HookState.build
拿到状态值,然后把_currentHookState指向下一个
。
把整个流程串起来,就是:
-
HookElement.build
首先将_currentHookState
重置为指向链表第一个。 -
HookElement.build
调用到HookWidget.build
。 - 在
HookWidget.build
内按顺序调用useXXX
,每调用一次useXXX
就把_currentHookState
指向下一个。 - 等待下一次
HookElement.build
,返回第一条执行。
至此,我们就明白了为什么前面说不能出现用条件语句包裹的useXXX
useHook1();
if(condition){
useHook2();
}
useHook3();
像上述代码。如果第一次调用condition
为true
。那么此后_hooks
链表就按顺序保存着HookState1
,HookState2
,HookState3
。那如果再次调用的时候condition
为false
。useHook2()
被跳过,useHook3()
被调用,但此时_currentHookState
却指向HookState2
,这就出问题了。如果Hook2
和Hook3
类型不一致则会抛异常,如果不幸它们类型一致则取到了错误的状态,导致不易察觉的问题。所以我们一定要保证每次调用useXXX
都是一致的。
总结
从以上对flutter_hooks的介绍可以看出,使用Hooks可以大大简化我们的开发工作,但是要注意一点,flutter_hooks并不能处理在Widget
之间传递状态这种情况,这时就需要将Hooks和Provider等状态管理工具结合使用。
flutter_hooks将React中火爆的Hooks移植到Flutter。使广大Flutter开发者也能体会到Hooks概念的强大。大前端的趋势就是各个框架的技术理念相互融合,我希望通过阅读本文也能使大家对Hooks技术在Flutter中的应用有一些了解。如果文中有什么错漏之处,抑或大伙有什么想法,都请在评论中提出来。