Flutter Hooks 使用及原理

前言

Hooks,直译过来就是"钩子",是前端React框架加入的特性,用来分离状态逻辑和视图逻辑。如今这个特性并不仅局限在于React框架中,其它前端框架也在借鉴。一样的,咱们也能够在Flutter中使用Hooks。Hooks对于从事Native开发的开发者可能比较陌生。但Flutter的一大优点就是综合了H5,Native等开发平台的优点,对Native开发者和对H5开发者都比较友好。因此经过这篇文章来介绍Hooks,但愿你们能对这一特性有所了解。前端

为何引入Hooks

咱们都知道在FLutter开发中的一大痛点就是业务逻辑和视图逻辑的耦合。这一痛点也是前端各个框架都有的痛点。因此你们就像出来各类办法来分离业务逻辑和视图逻辑,有MVP,MVVM,React中的Mixin,高阶组件(HOC),直到Hooks。Flutter中你们可能对Mixin比较熟悉,我以前写过一篇文章介绍使用Mixin这种方式来分离业务逻辑和视图逻辑。git

Mixin的方式在实践中也会遇到一些限制:github

  • Mixin之间可能会互相依赖。
  • Mixin之间可能存在冲突。

所以咱们引入Hooks来看看能不能避免Mixin的这些限制。数组

Flutter Hooks使用

引入Hooks须要在pubspec.yaml加入如下内容缓存

flutter_hooks: ^0.12.0
复制代码

Hooks函数通常以use开头,格式为useXXX。React定义了一些经常使用的Hooks函数,如useState,useEffect等等。bash

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的计数器和原生的计数器例子源码有什么样的区别。markdown

  • 首先原生的计数器由于要保存counter这个状态,因此使用的是一个StatefulWidgetcounter保存在对应的State中。而使用Hooks改造过的计数器却没有使用StatefulWidget,而是继承自HookWidget, 它实际上是一个StatelessWidget
class HooksExample extends HookWidget {
复制代码
  • 其次咱们看到计数器的状态counter是经过调用函数useState()获取到的。入参0表明初始值。
final counter = useState(0);
复制代码
  • 最后就是在点击事件的处理上,咱们只是把计数器数值+1。并无去调用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原理小结中作以说明。

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.disposeuseEffect的第二个入参是一个空数组。这样就保证了初始化和清理函数只会在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而且加上了HookElementmixin。因此关键的东西应该都是在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函数。在每次HookElementbuild的时候都会把_currentHookState指向_hooks链表的第一个元素。而后才走Widgetbuild函数。也就是说,每次重建Widget的时候都会重置_currentHookState。记住这一点。

另外一个问题。咱们不是在讨论Hooks吗?那这里的HookStateHook又是什么关系呢?

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一毛同样。咱们能够直接拿StatefulWidgetState的关系来理解HookHookState的联系了。有一点区别是State.build返回值是个Widget。而HookState.build的返回值则是状态值。

另外,一个StatefulElement只会持有一个State。而HookElement则可能持有多个HookState,而且把这些HookState都放在_hooks这个链表里。以下图所示:

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();
复制代码

像上述代码。若是第一次调用conditiontrue。那么此后_hooks链表就按顺序保存着HookState1,HookState2,HookState3。那若是再次调用的时候conditionfalseuseHook2()被跳过,useHook3()被调用,但此时_currentHookState却指向HookState2,这就出问题了。若是Hook2Hook3类型不一致则会抛异常,若是不幸它们类型一致则取到了错误的状态,致使不易察觉的问题。因此咱们必定要保证每次调用useXXX都是一致的。

总结

从以上对flutter_hooks的介绍能够看出,使用Hooks能够大大简化咱们的开发工做,可是要注意一点,flutter_hooks并不能处理在Widget之间传递状态这种状况,这时就须要将Hooks和Provider等状态管理工具结合使用。

flutter_hooks将React中火爆的Hooks移植到Flutter。使广大Flutter开发者也能体会到Hooks概念的强大。大前端的趋势就是各个框架的技术理念相互融合,我但愿经过阅读本文也能使你们对Hooks技术在Flutter中的应用有一些了解。若是文中有什么错漏之处,抑或大伙有什么想法,都请在评论中提出来。

相关文章
相关标签/搜索