Hooks,直译过来就是"钩子",是前端React框架加入的特性,用来分离状态逻辑和视图逻辑。如今这个特性并不仅局限在于React框架中,其它前端框架也在借鉴。一样的,咱们也能够在Flutter中使用Hooks。Hooks对于从事Native开发的开发者可能比较陌生。但Flutter的一大优点就是综合了H5,Native等开发平台的优点,对Native开发者和对H5开发者都比较友好。因此经过这篇文章来介绍Hooks,但愿你们能对这一特性有所了解。前端
咱们都知道在FLutter开发中的一大痛点就是业务逻辑和视图逻辑的耦合。这一痛点也是前端各个框架都有的痛点。因此你们就像出来各类办法来分离业务逻辑和视图逻辑,有MVP,MVVM,React中的Mixin,高阶组件(HOC),直到Hooks。Flutter中你们可能对Mixin比较熟悉,我以前写过一篇文章介绍使用Mixin这种方式来分离业务逻辑和视图逻辑。git
Mixin的方式在实践中也会遇到一些限制:github
所以咱们引入Hooks来看看能不能避免Mixin的这些限制。数组
引入Hooks须要在pubspec.yaml
加入如下内容缓存
flutter_hooks: ^0.12.0
复制代码
Hooks函数通常以use
开头,格式为useXXX
。React定义了一些经常使用的Hooks函数,如useState
,useEffect
等等。bash
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的计数器和原生的计数器例子源码有什么样的区别。markdown
counter
这个状态,因此使用的是一个StatefulWidget
。counter
保存在对应的State
中。而使用Hooks改造过的计数器却没有使用StatefulWidget
,而是继承自HookWidget
, 它实际上是一个StatelessWidget
。class HooksExample extends HookWidget {
复制代码
counter
是经过调用函数useState()
获取到的。入参0表明初始值。final counter = useState(0);
复制代码
setState()
,计数器就会自动刷新。onPressed:() => counter.value++
复制代码
可见相比于原生Flutter的模式,一样作到了将业务逻辑和视图逻辑分离。不须要再使用StatefulWidget
,就能够作到对状态的访问和维护。app
咱们也能够在同一个Widget
下引入多个Hooks:框架
final counter = useState(0);
final name = useState('张三');
final counter2 = useState(100);
复制代码
这里要特别注意的一点是,使用Hooks的时候不能够在条件语句中调用useXXX
,相似如下这样的代码要绝对避免。
if(condition) {
useMyHook();
}
复制代码
熟悉Hooks的同窗可能会知道这是为何。具体缘由我会在下面的Flutter Hooks原理小结中作以说明。
当你使用了BLoC或者MobX,可能须要有一个时机来建立对应的store。这时你可让useMemoized
来为你完成这项工做。
class MyWidget extends HookWidget {
@override
Widget build(BuildContext context) {
final store = useMemoized(() => MyStore());
return Scaffold(...);
}
}
复制代码
useMemoized
的入参是个函数,这个函数会返回MySotre
实例。此函数在MyWidget
的生命周期内只会被调用一次,获得的MySotre
实例会被缓存起来,后续再次调用useMemoized
会获得这一缓存的实例。
在首次建立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,flutter_hooks
还提供了一些能够节省咱们代码量的Hooks。如useAnimationController
,提供AnimationController
直接用而不用去操心初始化以及释放资源的事情。还有useTabController
,useTextEditingController
等等,完整Hooks列表你们能够去flutter_hooks@github查看。
当以上Hooks不能知足需求时,咱们也能够自定义Hooks。自定义Hooks有两种方式,一种是用函数来自定义自定义Hooks,若是需求比较复杂的话还能够用类来自定义Hooks。
这种方式通常来说就是用咱们自定义的函数来包裹组合原生的Hooks。好比对前面的计数器那个例子。咱们想在技术器增长的时候除了界面上有显示,还须要在日志里打出来。那么就能够这样来自定义一个Hook:
ValueNotifier<T> useLoggedState<T>(BuildContext context, [T initialData]) {
final result = useState<T>(initialData);
useValueChanged(result.value, (_, __) {
print(result.value);
});
return result;
}
复制代码
若是需求比较复杂,须要在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原理以前咱们要先提几个问题。在用Hooks改造计数器以后,就没有了StatefulWidget
。那么计数器的状态放在哪里了呢?在状态发生变化以后界面又是如何响应的呢?带着这些问题让咱们来探索Flutter Hooks的世界
首先来看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
:
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
又是什么关系呢?
abstract class Hook<R> {
const Hook({this.keys});
@protected
HookState<R, Hook<R>> createState();
}
复制代码
Hook
这个类就很简单了,并且看起来很像一个StatefulWidget
。那么对应的State
就是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
这个链表里。以下图所示:
至此咱们知道了引入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中的应用有一些了解。若是文中有什么错漏之处,抑或大伙有什么想法,都请在评论中提出来。