vlambda博客
学习文章列表

为啥Flutter Hooks没有受到太多关注和青睐?

作者 | Jimmy Aumard
译者 | 王强
策划 | 张晓楠
了解 Flutter Hooks 并不需要 React 的相关知识。

Flutter Hooks 虽然面世已经有一段时间了,但是迄今为止它并没有受到太多关注和青睐。我很奇怪为什么会是这个样子,毕竟它真的很好用!在本文中,我会试着告诉大家如何使用 Flutter Hooks 来减少样板代码,并基本上摆脱你现在用的几乎所有有状态小部件(StatefulWidget),让大家知道 Hooks 用起来是多么简单利落!

什么是 Hooks,它又是从何而来的?总不会是无名氏发明的吧?

其实 Hooks 最初是源于 React,但这里我并不会谈什么 React,因为我没用过它,以后也应该不会用的。换句话说了解 Flutter Hooks 并不需要 React 的相关知识。

Hooks 是一种与多个小部件共享同一代码的方法,这些代码往往是在有状态小部件之间重复或难以共享的代码。这里我的总结是:“ Hooks 是 UI 逻辑的管理者 ”。

接下来我会介绍自己在应用中使用最多的 Hooks,及其有状态小部件的等效形式,方便你对比两者并理解前者带来的实际收益。

Memoized Hook

这种 Hook(记忆化 Hook)是在小部件的生命周期中缓存对象实例的一种简单方法。用它可以轻松在页面上创建 BLoC、MobX 存储或通知程序对象。

下面是有状态小部件的版本:
class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage{
  final store = MyStore();

  _MyHomePageState();
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}
然后是 Hook 的等效版本:
class MyHomePage extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final store = useMemoized(() => MyStore());
    return Container();
  }
}

这两个示例都在小部件的生命周期内创建了一个 MyStore 实例,效果也是一样的。这里 Flutter Hooks 的优势并不大,但一般来说,当你希望初始化对象以加载数据的时候,用 Hooks 也是可以做到的。现在让我们看看 useEffect 。

Effect Hook

如前所述,我们要加载数据,为此一般会在 initState 上调用一个方法。

有状态小部件的版本:
class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage{
  final store = MyStore();

  _MyHomePageState();
@override
  void initState() {
    store.loadData();
    super.initState();
  }
@override
  Widget build(BuildContext context) {
    return Container();
  }
}
然后是等效的 Hook 版本:
class MyHomePage extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final store = useMemoized(() => MyStore());
    useEffect(() {
      store.loadData();
    }, const []);
    return Container();
  }
}
这里使用 useEffect 模拟 initState,并且在小部件的生命周期内仅被调用一次。如果需要,你还可以返回一个在放弃小部件时将调用的函数,如下所示:
useEffect(() {
  store.loadData();
  return store.dispose;
}, const []);

看起来不错吧?const[] 表示在未放弃(dispose)小部件之前,请勿调用 effect。你可以提供一组参数,当其中一个参数更改时将调用 effect。下面来看看另一个关于动画的例子。

动画 Hooks
下面是一个简单的示例,效果是在点击按钮时旋转一个框体:
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new MyHomePage(),
    );
  }
}
class MyHomePage extends StatefulWidget {
  MyHomePage({Key key}) : super(key: key);
  @override
  _MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePagewith SingleTickerProviderStateMixin {
  AnimationController controller;
  _MyHomePageState();
  @override
  void initState() {
    controller = AnimationController(vsync: this, duration: Duration(milliseconds: 800));
    super.initState();
  }
  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          RotationTransition(
            turns: controller,
            child: ColoredBox(
              color: Colors.red,
              child: SizedBox(
                width: 200,
                height: 200,
              ),
            ),
          ),
          FlatButton(
            onPressed: () {
              if (controller.isCompleted) {
                controller.reset();
              }
              controller.animateTo(controller.value + .25);
            },
            child: Text(
              'Rotate',
              style: TextStyle(color: Colors.red),
            ),
          ),
        ],
      ),
    );
  }
}
使用有状态小部件完成的基本旋转动画下面是 Hook 的等效版本:
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new MyHomePage(),
    );
  }
}
class MyHomePage extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final controller = useAnimationController(duration: Duration(milliseconds: 800));
    return Center(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          RotationTransition(
            turns: controller,
            child: ColoredBox(
              color: Colors.red,
              child: SizedBox(
                width: 200,
                height: 200,
              ),
            ),
          ),
          FlatButton(
            onPressed: () {
              if (controller.isCompleted) {
                controller.reset();
              }
              controller.animateTo(controller.value + .25);
            },
            child: Text(
              'Rotate',
              style: TextStyle(color: Colors.red),
            ),
          ),
        ],
      ),
    );
  }
}

我们可以看到,Hooks 为我们管理了控制器的生命周期,我们无需放弃控制器,也无需像有状态小部件中那样提供 ticker provider。Hooks 允许你创建自己的 Hooks,这意味着如果你找不到内置的 Hooks,则只需创建自己的版本即可。

下面我们看看如何创建一个管理 TabController 的 Hook。

定制 Hooks

flutter_hooks 包提供了两种自定义 Hooks 的方法,只需使用一个函数或创建一个自定义类即可。

首先,我们来看一个实现为函数的自定义 Hook:
TabController useTabController({@required int length, int initialIndex = 0}) {
  final tickerProvider = useSingleTickerProvider(keys: [length, initialIndex]);
  final controller = useMemoized(() => TabController(length: length, vsync: tickerProvider, initialIndex: initialIndex), [tickerProvider]);
  useEffect(() {
    return controller.dispose;
  }, [controller]);
  return controller;
}

这里我们拆开来看。要创建一个 TabController,我们需要一个 ticker provider,还需要 tab 的数量和当前 tab 的可选初始索引。这里的 ticker provider 由一个称为 useSingleTickerProvider 的已有 Hook 搞定。这一步容易,在使用我们的自定义 Hook 时必须同时提供 length 和 initialIndex。

你会看到有一组 keys 被传递给了 useSingleTickerProvider 。这是为了确保任意 key 被更改时都会重新创建 ticker provider。例如,当 tab 的数量变化时就会重新创建它。

我们需要缓存 TabController,使其在小部件生命周期中只有一次,所以我们要使用 useMemoized 。在这里,我们将 tickerProvider 传递为第二个参数,以便在 ticker 更改时(也就是在 length 或 initialIndex 更新时)重新创建控制器。这里依旧都是自动化的。

如前所见,要放弃 TabController,我们依靠 useEffect() 函数返回控制器的 dispose 方法。

请注意,如果提供了新的 TabController 作为第二个参数,那么这个方法也会被调用的。

那么定制 Hook 类呢?

由于 Hook 函数非常易于使用,因此我不需要将其作为一个类来实现,不过这里还是展示一下具体的做法。

当你的 Hooks 的复杂度增长时,就应将其作为一个类来实现;实际上,这个包的文档就是这样建议的。

将我们的 TabController Hook 作为自定义类实现是这个样子:
TabController useTabController({@required int length, int initialIndex = 0}) {
  return use(TabControllerHook(length, initialIndex));
}
class TabControllerHook extends Hook<TabController{
  final int length;
  final int initialIndex;
  const TabControllerHook(this.length, this.initialIndex);
  @override
  HookState<TabController, TabControllerHook> createState() {
    return _TabControllerHookState();
  }
}
class _TabControllerHookState extends HookState<TabControllerTabControllerHook{
  @override
  build(BuildContext context) {
    final tickerProvider = useSingleTickerProvider(keys: [hook.length, hook.initialIndex]);
    final controller = useMemoized(() => TabController(length: hook.length, vsync: tickerProvider, initialIndex: hook.initialIndex), [tickerProvider]);
    useEffect(() {
      return controller.dispose;
    }, [controller]);
    return controller;
  }
}

这里你也看到了,一个 Hook 就像一个有状态小部件一样运行!你有一个有状态类,即 HookState 类,可以访问自定义 Hook 类的字段(此处为 hook.length )。而 hookState 的构建方法将构建你的 Hook 的结果。所以这些做起来还是很容易的。Hooks 提供的不仅仅是这些捷径。例如,它可以管理 FocusNode 或 TextEditingController 来帮助你处理表单。可以访问官方文档以了解更多信息。

我喜欢 Hooks,并在我的所有项目中都使用它。我通常将它与 Provider 和 MobX 结合使用。

你可以在 pub 上找到 Hooks,附带的文档都很完善。

https://pub.dev/packages/flutter_hooks

 延伸阅读

https://medium.com/flutter-community/flutter-hooks-say-goodbye-to-statefulwidget-and-reduce-boilerplate-code-8573d4720f9a







关于开源科技


开源科技 OSTech 具备多年且成熟的科技互联网社区生态建设以及开源生态合作体系,有专业的企业定制技术培训、互联网(开发者)社区生态建设与推广、科技产品推广及数字营销、科技活动峰会策划等服务。长期与Linux Foundation等科技公司保持开发者生态合作,发起组织各类科技活动和开源动作。