Flutter Hooks 使用及原理

3,484 阅读7分钟

为什么使用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函数里组合了useStateuseValueChanged,来完成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中的应用有一些了解。如果文中有什么错漏之处,或者大伙有什么想法,请在评论中提出来,互相探讨,不胜感激。