为什么使用Hooks
在Flutter项目的开发中,我们很容易发现业务逻辑和视图逻辑的高度耦合使我们非常难受,这个痛点也是前端开发经常遇到的。为了解决这个问题,在Flutter中其实也可以使用Mixin方式来解决,但是在实践中会遇到一些限制:
- Mixin之间可能会互相依赖。
- Mixin之间可能存在冲突。 Hooks可以解决这些限制,下面就引入Hooks来一探究竟吧!
怎么样使用Hooks
引入Hooks需要在 pubspec.yaml 中加入以下内容
dependencies:
flutter_hooks: ^0.14.1
执行下载包命令
$ flutter pub get
在用到的Dart包里引入
import 'package:flutter_hooks/flutter_hooks.dart';
接下来就可以使用flutter_hooks了,记住flutter_hooks的方法名都是以use开头的,常用的有useState,useEffect等等。
-
useState就像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), ), ); } }
从表面上看,HooksExample继承了HookWidget,其实HookWidget继承了一个StatelessWidget,所以HooksExample是一个StatelessWidget。那为什么会改变状态了呢?不是只有StatefulWidget才能改变状态吗?带着这样的疑问,我们仔细观察一下,不难发现从上面的代码里我们看到有俩句是hooks的方法,那答案应该就在这里,useState(0)设置初值为0,counter.value++实现数字的递增。这么简单就做到了将业务逻辑和视图逻辑分离?不需要再使用StatefulWidget,就可以做到对状态的访问和维护?先保持着疑问,后面我们再来揭开这俩个方法的背后的内幕。
当然,我们也可以在同一个Widget下引入多个Hooks:
final counter = useState(0);
final name = useState('张三');
final counter2 = useState(100);
这里要特别注意的一点是,使用Hooks的时候不可以有条件语句,像下面这样决不可以:
if(condition) {
useMyHook();
}
-
useEffect这个方法是用在需要对一个对象进行初始化和Widget生命周期结束的时候做一些清理工作的时候,看下面的代码,你就会恍然大悟:
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生命周期开始和结束时各被调用一次。如果不传这个参数会发生crash。
除了以上这些Hooks,flutter_hooks还提供了一些可以节省我们代码量的Hooks。如useMemoized、useValueChanged、useAnimation、useFuture等等,完整Hooks列表及使用方法大家可以去flutter_hooks@github查看。
-
自定义hook当flutter_hooks提供的hook不能满足我们的要求时,还可以有俩种方式去自定义hook。
1、A function
到目前为止,function是自定义hook最常用的方法,因为hook本来就是可以组合的,所以在自定义的function里可以组合hook,来创建自定义的hook。当然自定义的hook也要以use开头。
下面是自定义一个hook,传入一个变量,若发生了变化,会打印出来。
ValueNotifier<T> useLoggedState<T>(BuildContext context, [T initialData]) { final result = useState<T>(initialData); useValueChanged(result.value, (_, __) { print(result.value); }); return result; }很明显,useLoggedState函数里组合了
useState和useValueChanged,来完成useLoggedState的功能。2、A class
当hook太复杂时,可以自定义一个继承Hook的类,这样这个类看起来特别像一个State,可以访问一些生命周期的方法initHook、dispose、setState,像下面这样把一个类隐藏在一个函数里是非常好的做法:
Result useMyHook(BuildContext context) { return use(const _TimeAlive()); }下面是自定义的hook,用来打印一个state存活的时间:
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(); } }
探一探Hooks的底层实现原理
带着前面的疑问,我们来看看底层实现的原理。那从useState开始吧!
ValueNotifier<T> useState<T>([T initialData]) {
return use(_StateHook(initialData: initialData));
}
R use<R>(Hook<R> hook) => Hook.use(hook);
在useState方法里,调用了use方法,并创建了_StateHook对象做为参数,跟进去一看,不仅有_StateHook,还有_StateHookState,这好眼熟啊,不就是StatefullWidget吗!再仔细看看更像了,这不就是自定义的StatefullWidget吗!那_StateHook继承的Hook,_StateHookState继承的HookState,不就是Widget和State吗!!!那这样理解就很好往下走了啊。。。下面是它们四个类的代码(只展示一些关键代码):
Hook类:
abstract class Hook<R> {
const Hook({this.keys});
static R use<R>(Hook<R> hook) {
return HookElement._currentHookElement._use(hook);
}
@protected
HookState<R, Hook<R>> createState();
}
_StateHook类:
class _StateHook<T> extends Hook<ValueNotifier<T>> {
const _StateHook({this.initialData});
final T initialData;
@override
_StateHookState<T> createState() => _StateHookState();
}
HookState类:
abstract class HookState<R, T extends Hook<R>> {
@protected
BuildContext get context => _element;
HookElement _element;
T get hook => _hook;
T _hook;
void deactivate() {}
@protected
void setState(VoidCallback fn) {
fn();
_element
.._isOptionalRebuild = false
..markNeedsBuild();
}
}
_StateHookState类:
class _StateHookState<T> extends HookState<ValueNotifier<T>, _StateHook<T>> {
ValueNotifier<T> _state;
@override
void initHook() {
super.initHook();
_state = ValueNotifier(hook.initialData)..addListener(_listener);
}
@override
void dispose() {
_state.dispose();
}
@override
ValueNotifier<T> build(BuildContext context) {
return _state;
}
void _listener() {
setState(() {});
}
}
initialData保存在_StateHook对象里,调用了Hook类的use方法,从上面可以看到又调用了HookElement类的_use方法。下面我们来看看HookElement又是啥。
mixin HookElement on ComponentElement {
static HookElement _currentHookElement;
_Entry<HookState> _currentHookState;
final LinkedList<_Entry<HookState>> _hooks = LinkedList();
LinkedList<_Entry<bool Function()>> _shouldRebuildQueue;
LinkedList<_Entry<HookState>> _needDispose;
bool _isOptionalRebuild = false;
Widget _buildCache;
@override
Widget build() {
// Check whether we can cancel the rebuild (caused by HookState.mayNeedRebuild).
final mustRebuild = _isOptionalRebuild != true ||
_shouldRebuildQueue.any((cb) => cb.value());
_isOptionalRebuild = null;
_shouldRebuildQueue?.clear();
if (!mustRebuild) {
return _buildCache;
}
if (kDebugMode) {
_debugIsInitHook = false;
}
_currentHookState = _hooks.isEmpty ? null : _hooks.first;
HookElement._currentHookElement = this;
try {
_buildCache = super.build();
} finally {
_unmountAllRemainingHooks();
HookElement._currentHookElement = null;
if (_needDispose != null && _needDispose.isNotEmpty) {
for (var toDispose = _needDispose.last;
toDispose != null;
toDispose = toDispose.previous) {
toDispose.value.dispose();
}
_needDispose = null;
}
}
return _buildCache;
}
R _use<R>(Hook<R> hook) {
/// At the end of the hooks list
if (_currentHookState == null) {
_appendHook(hook);
} else if (hook.runtimeType != _currentHookState.value.hook.runtimeType) {
final previousHookType = _currentHookState.value.hook.runtimeType;
_unmountAllRemainingHooks();
if (kDebugMode && _debugDidReassemble) {
_appendHook(hook);
} else {
}
} 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;
}
}
在_use方法里,我们可以看到俩处关键的点:
1、一个是_appendHook(hook);进入到_appendHook方法,发现进入到了HookElement的类扩展,这在这个方法,调用了创建HookState的方法,并调用了HookState的initHook()方法,在initHook()里包装了数据并赋值给_state并且还把_listener添加进了_listeners,那么_listener是什么,在这个类的最下面我们看到了原来是setState(() {})这下是不是好像感觉到了什么,哈哈,最重要的是它把当前的HookElement对象赋给了HookState的_element属性。那当前的HookElement是哪个呢?带着这个疑问往下看。
2、另一个是_currentHookState.value.build(this) as R;_currentHookState是一个_Entry类型的变量,所以就是调用了HookState子类的build方法,从HookState类里的代码可以看到是调用了HookState的子类_StateHookState的build方法。到此可以看到,外部调用useState最终返回值在此处产生。
extension on HookElement {
HookState<R, Hook<R>> _createHookState<R>(Hook<R> hook) {
assert(() {
_debugIsInitHook = true;
return true;
}(), '');
final state = hook.createState()
.._element = this
.._hook = hook
..initHook();
assert(() {
_debugIsInitHook = false;
return true;
}(), '');
return state;
}
void _appendHook<R>(Hook<R> hook) {
final result = _createHookState<R>(hook);
_currentHookState = _Entry(result);
_hooks.add(_currentHookState);
}
}
下面我们再从counter.value++这儿进去看看发生了什么,进去一看就到了ValueNotifier这个类,set方法里我们看到,当值发生变化时,会调用父类方法notifyListeners(),
class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> {
ValueNotifier(this._value);
@override
T get value => _value;
T _value;
set value(T newValue) {
if (_value == newValue)
return;
_value = newValue;
notifyListeners();
}
@override
String toString() => '${describeIdentity(this)}($value)';
}
ValueNotifier父类方法:
void notifyListeners() {
assert(_debugAssertNotDisposed());
if (_listeners != null) {
final List<VoidCallback> localListeners = List<VoidCallback>.from(_listeners);
for (final VoidCallback listener in localListeners) {
try {
if (_listeners.contains(listener))
listener();
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'foundation library',
context: ErrorDescription('while dispatching notifications for $runtimeType'),
informationCollector: () sync* {
yield DiagnosticsProperty<ChangeNotifier>(
'The $runtimeType sending notification was',
this,
style: DiagnosticsTreeStyle.errorProperty,
);
},
));
}
}
}
}
从notifyListeners方法中可以看到会调用之前保存的setState(() {})方法,接着会调用HookState的保存的_element对象的markNeedsBuild()方法,最终调用HookElement的build方法进行重新渲染界面。突然间有没有发现这儿也出现了HookElement,和前面让我们疑问的HookElement是同一个,但是HookElement是啥时候创建的?猜一下,会不会是HookWidget的Element?那下面我们就来看看HookWidget这个类做了什么。
abstract class HookWidget extends StatelessWidget {
/// Initializes [key] for subclasses.
const HookWidget({Key key}) : super(key: key);
@override
_StatelessHookElement createElement() => _StatelessHookElement(this);
}
class _StatelessHookElement extends StatelessElement with HookElement {
_StatelessHookElement(HookWidget hooks) : super(hooks);
}
HookWidget这个类很简单,构造完HookWidget就会隐式调用createElement方法,这时会生成_StatelessHookElement对象,接着会调用StatelessElement的mount方法,然后调用HookElement的build方法,从前面关于HookElement的代码中我们看到build方法中的这一句代码HookElement._currentHookElement = this,前面的疑问都明明白白了吧。最终调用StatelessElement里的widget.build(this);这里有关于底层渲染的逻辑没有细说,可以看我另一篇文章Widget的渲染原理。
总结一下
以上的探讨后,我们可以理一下整个逻辑,通过继承HookWidget创建了一个StatelessWidget和StatelessElement,StatelessElement通过mix HookElement可以管理多个state,每一个use都会创建一个State来让HookElement管理,当值发生变化时,调用State的setState方法,让HookWidget的HookElement调用HookWidget的Build方法,从而实现重新渲染。flutter_hooks到底hoook了啥,不就是Element吗,您说呢?
虽然使用Hooks可以大大简化我们的开发工作,但是要注意一点,flutter_hooks并不能处理在Widget之间传递状态这种情况,这时就需要将Hooks和Provider等状态管理工具结合使用。
最后我希望通过阅读本文能使大家对Hooks技术在Flutter中的应用有一些了解。如果文中有什么错漏之处,或者大伙有什么想法,请在评论中提出来,互相探讨,不胜感激。