Loading...
墨滴

mxx

2021/04/02  阅读:24  主题:科技蓝

通过Flutter hooks简化代码

(看了Resocoder和Robert Brunhage的两个视频, 这篇总结一下其中的内容, 我其实还没有实践过)

The problem

Flutter hooks想解决的问题是StatefulWidget的一些常见pattern太复杂, 减少了代码的可读性("readability").

一个典型例子就是animation controller:

class MyPage extends StatefulWidget{
  @override
  _MyPageState createState() => _MyPageState();
}

class _MyPageState extends State<MyPage>
    with SingleTickerProviderStateMixin 
{
  AnimationController _animController;

  @override
  void initState() {
    super.initState();
    _animController = AnimationController(
      vsync: this,
      duration: kThemeAnimationDuration,
      value: 1// initially visible
    );
  }

  @override
  Widget build(BuildContext context) {
    // use _animController here
  }

  @override
  void dispose() { 
    _animController.dispose();
    super.dispose();
  }
}

这种pattern在vanilla flutter里经常被使用, 但是对同一个controller在好几个地方操作(initiState()/setState()/dispose()), 真的不好读.

Flutter hooks就可以把这种重复的pattern抽离出来, 把StatefulWidget变成类似StatelessWidgetHookWidget, 简化代码, 大大提高可读性.

(btw 这个包的作者是Rémi Rousselet大神, 质量有保障👌)

使用现成的Hooks

对于常用的pattern有现成的hooks可以直接套用. 比如上面的animation controller例子, 用flutter hooks可以写成这样:

class MyPage extends HookWidget {  //! StatelessWidget => HookWidget
  @override
  Widget build(BuildContext context) {
    final animController = useAnimationController(
        duration: kThemeAnimationDuration, 
        initialValue: 1,
    );
    // use _animController here
  }
}

Flutter hooks自带了不少现成的hooks[1], 有面向Animation的, 有面向Stream的等等挺多, 有时间可以好好看一看...

自定义Hook

cf. Resocoder的教程: https://resocoder.com/2020/01/21/flutter-hooks-hide-fab-animation-100-widget-code-reuse/#t-1616317538465

如果现成的hook不满足条件, 其实自己实现起来也非常容易, 基本就是把原来StatefulWidget/State<Foo>的内容变成Hook<R>/HookState<R,Foo>. 其中Foo是我们自定义的类名, R则是state的类型. 比如对于之前的例子: Foo=MyPage, R=AnimationController.

这里列一下二者的区别, 基本是一一对应的, 所以迁移起来不难:

StatefulWidget v.s. Hook:

StatefulWidget Hook<R>
class Foo extends StatefulWidget class Foo extends Hook<R>
createState() => State<Foo> createState() => HookState<R>

State v.s. HookState:

State<Foo> HookState<R, Foo>
R _data; R _data;
initState() {_data=...} initHook() {_data=...}
build(ctx) => Widget build(ctx) => R
dispose() dispose()
widget.xxx hook.xxx

当自定义的Hook/HookState写好以后, 根据Flutter hooks的惯例, 需要写一个useXxHooks的函数, 返回类型是R:

R useMyHooks(...) {
  return Hook.use(_FooHook());
}

useState/useEffect

cf. Robert Brunhage的视频: https://www.youtube.com/watch?v=A1DUBgIsCv8

对于不追求逻辑复用, 只想去掉initState()/dispose()的场景, 可以考虑直接把一个StatefulWidget变成HookWiget. Flutter hooks提供了useStateuseEffect, 让我们可以直接在HookWiget.build()函数里面创建/修改状态.

创建状态: 用`useState(R initialVal)`[2].
注意返回值的是一个ValueNotifier<R> (cf. [[202102212026 ChangeNotifier, ValueNotifier and StateNotifier|各种notifier]]), 获得包含的值需要.value.

修改状态: `useEffect()`[3], 有两个参数:

  • 第一个参数(effect())是一个函数Dispose? Function()
    • 可以把修改状态的内容(比如原先setState的内容)放在这里
    • effect可以返回另一个函数(Dispose?), 这个函数会_在effect()下次一被调用或者当widget dispose的时候运行_.
  • 第二个可选参数keys是一个list, 控制effect()何时被调用:
    • 如果为null(默认), 则_每次build()_ 都会调用effect()
    • 如果非null, 则只在_第一次build(), 以及当任何keys元素改变时_才调用effect()
      • 注意: keys可以是空白list, 但非null, 比如keys=[], 此时effect只运行一次.

这种方法可以把一个StatefulWidget简化为类似StatelessWidgetHookWidget, 简化代码. 例如用useState/useEffect写一个简单计时器, 代码相比vanilla flutter要简洁许多:

class MyTimerPage extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final _numberNotifier = useState(0);  //! Create state

    useEffect(  //! Change state
      /*effect=*/() {
        //! ~= initState()
        final timer = Timer.periodic(
          Duration(seconds: 1), 
          //! ~= setState()
          (time) => _numberNotifier.value = time.tick,
        );
        return timer.cancel;  //! ~= dispose()
      }, 
      /*keys=*/const [],  //! => effect() is called only once
    );

    return Scaffold(
      body: Center(
        child: Text(_numberNotifier.value.toString()),
      ),
    );
  }
}

当然这样写的缺点就是逻辑无法复用, 要复用逻辑的话可以参考上一节"自定义Hook":

  1. 把useState/useEffect的内容放进一个_MyTimerHook extends Hook<int>

  2. 定义int useMyTimer()函数:

    int useMyTimer() {
      return Hook.use(_MyTimerHook());
    }
  3. HookWidget里使用useMyTimerHook:

    class MyTimerPage extends HookWidget {
      @override
      Widget build(BuildContext context) {
        final number = useMyTimer();
        return Text(number.toString());
      }
    }

最佳实践

pub上列出了几条flutter hooks 最佳实践[4]:

  1. 所有hook的函数都以'use'开头:

    Widget build(BuildContext context) {
      // starts with `use`, good name
      useMyHook();
      // doesn't start with `use`, could confuse people into thinking that this isn't a hook
      myHook();
      // ....
    }
  2. 不要在分支条件里使用hook

    Widget build(BuildContext context) {
      useMyHook();  //! Good: DO call hooks unconditionally
      // ....
    }

    Widget build(BuildContext context) {
      if (condition) {
        useMyHook();  //! BAD: DON'T wrap use into a condition
      }
      // ....
    }

参考内容:

参考资料

[1]

现成的hooks: https://pub.dev/packages/flutter_hooks#existing-hooks

[2]

useState(R initialVal): https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useState.html

[3]

useEffect(): https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useEffect.html

[4]

最佳实践: https://pub.dev/packages/flutter_hooks#rules

mxx

2021/04/02  阅读:24  主题:科技蓝

作者介绍

mxx