Flutter Hooks之useState(一)

1,861 阅读6分钟

冗余代码是编程之大忌,如何优雅的减少冗余代码,增加摸鱼时长是每个程序员都要追求的目标之一。今天给大家介绍一款Flutter开发摸鱼小工具--Flutter Hooks,官方地址:pub.flutter-io.cn/packages/fl…

我们从一个入门案例说起,点击按钮增加计数,因为需要更新UI,使用StatefulWidget

class Counter extends StatefulWidget {
  const Counter({Key? key}) : super(key: key);

  @override
  State<Counter> createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int count = 0;

  @override
  Widget build(BuildContext context) {
    print("rebuild 1");
    return Scaffold(
        appBar: AppBar(
          title: const Text('useState example'),
        ),
        body: GestureDetector(
          onTap: () {
            // 使页面重建
            setState(() {
              count++;
            });
          },
          child: Center(
            child: Text("tap ${count} times"),
          ),
        ));
  }
}

代码的重点就是setState方法,当我们需要更新页面的时候,调用此方法会引起系统调用build方法重建此Widget树,那有没有什么办法,当count发生变动时,可以自动更新Widget。其实是有的,Flutter Hooks中的useState就提供了一个这样的实现。

Hook的使用

  • 引入版本 flutter_hooks: ^0.12.0
  • 继承HookWidget、StatefulHookWidget
  • 使用useState方法即可
class Counter 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(
	  // 当counter中的值发生变化,会通知组件更新,不需要我们手动调用setState
        onPressed:() => counter.value++,
        child: const Icon(Icons.add),
      ),
    );
  }
}

上面使用Hook技术,运行后有同样的效果,但有几点比较奇怪

  • 没有State代码,HookWidget是继承StatelessWidget,怎么更新页面的
  • useState方法是重复调用的,但并没有重复初始化,这是为什么。

在继续分析之前,先看一下还有哪些用法,比如你想提升一下渲染效率,不想整个Counter重建,可以把body分离出去,这样每次点击只会重建Body组件

class Counter extends StatelessWidget {
  const Counter3({super.key});

  @override
  Widget build(BuildContext context) {
    print("Counter rebuild 1");
   return Scaffold(
    appBar: AppBar(
      title: const Text('useState example'),
    ),
    body: Body());
  }
}

class Body extends HookWidget{
  const Body({super.key});

  @override
  Widget build(BuildContext context) {
    print("Body rebuild");
    ValueNotifier<int> count = useState(0);
    return GestureDetector(
      onTap: () {
        count.value++;
      },
      child: Center(
        child: Text("tap ${count.value} times"),
      ),
    );
  }
}

只是这样写还挺费事,Hook提供了HookBuilder组件,继承自HookWidget,能实现同样效果

class Counter extends StatelessWidget {
  const Counter({super.key});

  @override
  Widget build(BuildContext context) {
    print("Counter rebuild 1");
    return Scaffold(
        appBar: AppBar(
          title: const Text('useState example'),
        ),
        body: HookBuilder(builder: (context) {
          print("Counter rebuild 2");
          ValueNotifier<int> count = useState(0);
          return GestureDetector(
            onTap: () {
              count.value++;
            },
            child: Center(
              child: Text("tap ${count.value} times"),
            ),
          );
        }));
  }
}
Hook组件关系图

关于Hook的使用不是本文介绍的重点,我们重点分析其实现原理。在上面案例中,使用Hook的地方都涉及到了HookWidget,我们从这组件入手,了解Hook的整体架构。如下图,Hook提供了两个组件:HookWidget或StatefulHookWidget,其实它们就是直接继承了StatelessWidget或StatefulWidget

688.png

abstract class HookWidget extends StatelessWidget {
  /// Initializes [key] for subclasses.
  const HookWidget({Key? key}) : super(key: key);

  @override
  _StatelessHookElement createElement() => _StatelessHookElement(this);
}

abstract class StatefulHookWidget extends StatefulWidget {
  /// Initializes [key] for subclasses.
  const StatefulHookWidget({Key? key}) : super(key: key);

  @override
  _StatefulHookElement createElement() => _StatefulHookElement(this);
}

继承的重点是复写了createElement方法,分别返回了_StatelessHookElement和_StatefulHookElement对象,而这两个对象也分别继承了StatelessElement和StatefulElement,还有一个HookElement,其实啥都没干

class _StatelessHookElement extends StatelessElement with HookElement {
  _StatelessHookElement(HookWidget hooks) : super(hooks);
}

class _StatefulHookElement extends StatefulElement with HookElement {
  _StatefulHookElement(StatefulHookWidget hooks) : super(hooks);
}

这里的with HookElement是一个mixin技术,如果不用这个mixin,应该在_StatelessHookElement和_StatefulHookElement中对父类方法进行复写,但两个类的复写代码是雷同的,而mixin技术就是为了在多个类中复用代码而生,所以这两个类都混入继承了HookElement,完成了对父类的复写,那么HookElement就是实现的重点。我们继承HookWidget或StatefulHookWidget就是为了使用HookElement

hook_element.png

从下面代码看,HookElement使用on关键字限定了ComponentElement,它有继承作用,HookElement可以使用super访问ComponentElement方法,也有限定作用,就是要混入这类,必须实现或继承了ComponentElement,_StatefulHookElement和_StatelessHookElement都是符合条件的。

Hook初始化

Counter组件继承了HookWidget就引入了HookElement,当应用启动,控件树开始构建,对于我们的组件来说,首先调用到的是HookElement的build方法,其中的super.build()调用的是ComponentElement的build方法,

mixin HookElement on ComponentElement {
   ...
  _Entry<HookState> _currentHookState;
  final LinkedList<_Entry<HookState>> _hooks = LinkedList();
  ...
  
    @override
  Widget build() {
    ...
    _currentHookState = _hooks.isEmpty ? null : _hooks.first;
    // 静态方法中保持了当前实例,useState方法中要用到
    HookElement._currentHookElement = this;
    // 调用父类方法,最后是调用Counter组件的build方法
    _buildCache = super.build();
    ...
    return _buildCache;
  }
}

ComponentElement的build方法被StatelessElement复写,也就调用到了Counter的build方法

class StatelessElement extends ComponentElement {
  /// Creates an element that uses the given widget as its configuration.
  StatelessElement(StatelessWidget super.widget);

  @override
  Widget build() => (widget as StatelessWidget).build(this);

  ...
}

在Counter组件的build方法中使用了useState方法

ValueNotifier<int> counter = useState(0);

useState使用的是_StateHook,如下图,继承自Hook

ValueNotifier<T> useState<T>(T initialData) {
  return use(_StateHook(initialData: initialData));
}

hook.png 除图中列出的一些,还有_TextEditingControllerHook等内置的Hook,它们的重点在于复写createState方法,返回HookState的子类,如下图,我们使用的_StateHook返回的就是_StateHookState

hook_state.png

HookState有两个重要的成员变量HookElement和Hook,如下图,HookElement有一个HookState链表,说明它是支持多个Hook同时使用的,这个对我们使用是有限制的

hook_state_element.png

到此梳理一下Hook组件的关系图,大致如下,菱形箭头表示组合关系,三角形是继承关系

Hook_all.png

use方法最后调用的是HookElement._currentHookElement对象,它是之前HookElement build方法使用当前hookElement对象初始化的静态成员变量,就是为了在这里方便调用

  static R use<R>(Hook<R> hook) {
    return HookElement._currentHookElement!._use(hook);
  }

这样我们就走到了当前HookElement的use方法

 R _use<R>(Hook<R> hook) {
    /// 第一次build时,_currentHookState为空
    if (_currentHookState == null) {
      _appendHook(hook);
    } else if (hook.runtimeType != _currentHookState!.value.hook.runtimeType) {
      ...
    } else if (hook != _currentHookState!.value.hook) {
      ...
    }

   // result是暴露给用户使用的,对于useState来说,返回的就是ValueNofity
    final result = _currentHookState!.value.build(this) as R;
   ...
   // 指向链表的下一个节点,如果没有就是空,这样后面接着调用use方法,就会继续走_appendHook
    _currentHookState = _currentHookState!.next;
    return result;
  }

组件第一次build重点看_appendHook,这里使用hook创建了HookState,并初始化了其hookElement和hook成员变量,然后把HookState封装成Entry添加到链表结尾

void _appendHook<R>(Hook<R> hook) {
  final result = _createHookState<R>(hook);
  _currentHookState = _Entry(result);
  // 将新建的HookState添加到链表尾部
  _hooks.add(_currentHookState!);
}


HookState<R, Hook<R>> _createHookState<R>(Hook<R> hook) {
 // 这里hook创建了HookState并完成一些初始化,
 // 这样每个HookState都持有HookElement和Hook的引用,而HookElement通过链表持用所有HookState引用
  final state = hook.createState()
    .._element = this
    .._hook = hook
    ..initHook();

  return state;
}

上面hook.createState()返回的就是_StateHookState,其build方法返回给用户的是ValueNotifier,它才是持用数据的地方,当我们调用counter.value++,其持用状态值发生变化,_listener方法就会被调用,然后调用setState方法。

class _StateHookState<T> extends HookState<ValueNotifier<T>, _StateHook<T>> {
  late final _state = ValueNotifier<T>(hook.initialData)
    ..addListener(_listener);

  @override
  void dispose() {
    _state.dispose();
  }

  @override
  ValueNotifier<T> build(BuildContext context) => _state;

  void _listener() {
    setState(() {});
  }
}

这里的setState方法继承自HookState,HookState是持用HookElement引用的,可以调用element的markNeedsBuild方法引起组件重建,事实上,StatefulWidget的setState也是这么干的,element才是更新组件的关键而不是Widget。

void setState(VoidCallback fn) {
  fn();
  _element!
    .._isOptionalRebuild = false
    ..markNeedsBuild();
}

在上面还提出一个问题,为什么Counter组件重复build,不会将useState里面的值重新初始化为0。首先当我们调用hookElement的markNeedsBuild方法将自己标记为dirty后,系统重建控件树,hookElement的build方法先被系统调用,此时链表已不为空了_currentHookState会被初始化

mixin HookElement on ComponentElement {
 
    @override
  Widget build() {
    // 先初始化当前hookState
    _currentHookState = _hooks.isEmpty ? null : _hooks.first;
    ...
    return _buildCache;
  }
}

然后是useState方法被调用,

  • _currentHookState在上面初始化了,不为空;
  • hook是新的但runtimeType类型没变;
  • hook是新的,但keys参数使用的是默认值,是一样的,走的是更新Hook逻辑,HookState还是之前的,所以返回的result还是之前的
 R _use<R>(Hook<R> hook) {
    /// 再build时,_currentHookState不为空
    if (_currentHookState == null) {
      _appendHook(hook);
    } else if (hook.runtimeType != _currentHookState!.value.hook.runtimeType) {
		// 抛出异常...
    } else if (hook != _currentHookState!.value.hook) {
      final previousHook = _currentHookState!.value.hook;
	  // 通过比较hook的keys决定是否保留HookState
	  if (Hook.shouldPreserveState(previousHook, hook)) {
	  	// 如果保留就更新一下hook
		  _currentHookState!.value
		  .._hook = hook
		  ..didUpdateHook(previousHook);
	  } else {
	  	// 如果不保留就创建新的HookState替换,并回收之前的
		  _needDispose ??= LinkedList();
		  _needDispose!.add(_Entry(_currentHookState!.value));
		  _currentHookState!.value = _createHookState<R>(hook);
	  }
    }

    // result是暴露给用户使用的,对于useState来说,返回的就是ValueNofity
    final result = _currentHookState!.value.build(this) as R;
   ...
    // 初始化下一个
    _currentHookState = _currentHookState!.next;
    return result;
  }

为什么上面use方法中要判断runtimeType呢,上面说过,HookElement是支持多个Hook的,我们在HookWidget中可以像下面这样使用多个,HookElement中的链表是按顺序存取HookState,一 一对应的

class Counter extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final counter = useState(0);
    final animationController = useAnimationController();
    final textEditControl = useTextEditingController();
   
    return ...;
  }
}

但如果你使用了条件语句,就可能破环了这种对应关系,导致hook.runtimeType前后不一致,所以使用Hook的前提就是不要将其放在条件语句中

class Counter extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final counter = useState(0);
    if(condition) {final animationController = useAnimationController ();}
    final textEditControl = useTextEditingController();
   
    return ...;
  }
}

到此,我们以setState为例,分析这个Hook的工作原理就基本结束了,最后总结一下大致的流程

setStateFlow.png