Flutter Hooks:为何未能吸引更多关注?

发表时间: 2020-08-02 14:55

了解 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<MyHomePage> with 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 更改时(也就是在 lengthinitialIndex 更新时)重新创建控制器。这里依旧都是自动化的。

如前所见,要放弃 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<TabController, TabControllerHook> {  @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