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

2020 年 8 月 9 日 InfoQ
作者 | 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<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


InfoQ 读者交流群上线啦!各位小伙伴可以扫描下方二维码,添加 InfoQ 小助手,回复关键字“进群”申请入群。大家可以和 InfoQ 读者一起畅所欲言,和编辑们零距离接触,超值的技术礼包等你领取,还有超值活动等你参加,快来加入我们吧!




点个在看少个 bug 

登录查看更多
0

相关内容

【NeurIPS 2020】基于因果干预的小样本学习
专知会员服务
70+阅读 · 2020年10月6日
专知会员服务
47+阅读 · 2020年10月5日
流畅的Python 中英文版 PDF 高清电子书
专知会员服务
83+阅读 · 2020年8月2日
《强化学习—使用 Open AI、TensorFlow和Keras实现》174页pdf
专知会员服务
139+阅读 · 2020年3月1日
五篇 ICCV 2019 的【图神经网络(GNN)+CV】相关论文
专知会员服务
15+阅读 · 2020年1月9日
【电子书】C++ Primer Plus 第6版,附PDF
专知会员服务
88+阅读 · 2019年11月25日
【电子书】Flutter实战305页PDF免费下载
专知会员服务
23+阅读 · 2019年11月7日
微信小程序支持webP的WebAssembly方案
前端之巅
19+阅读 · 2019年8月14日
美团:基于跨平台框架Flutter的动态化平台建设
前端之巅
14+阅读 · 2019年6月17日
2020年你应该知道的8种前端JavaScript趋势和工具
前端之巅
5+阅读 · 2019年6月9日
从webview到flutter:详解iOS中的Web开发
前端之巅
5+阅读 · 2019年3月24日
React Native 分包哪家强?看这文就够了!
程序人生
13+阅读 · 2019年1月16日
Python为啥这么牛?
Python程序员
3+阅读 · 2018年3月30日
代码这样写不止于优雅(Python版)
数说工作室
4+阅读 · 2017年7月17日
Arxiv
5+阅读 · 2019年11月22日
Arxiv
3+阅读 · 2018年8月27日
Arxiv
6+阅读 · 2018年2月26日
VIP会员
相关VIP内容
【NeurIPS 2020】基于因果干预的小样本学习
专知会员服务
70+阅读 · 2020年10月6日
专知会员服务
47+阅读 · 2020年10月5日
流畅的Python 中英文版 PDF 高清电子书
专知会员服务
83+阅读 · 2020年8月2日
《强化学习—使用 Open AI、TensorFlow和Keras实现》174页pdf
专知会员服务
139+阅读 · 2020年3月1日
五篇 ICCV 2019 的【图神经网络(GNN)+CV】相关论文
专知会员服务
15+阅读 · 2020年1月9日
【电子书】C++ Primer Plus 第6版,附PDF
专知会员服务
88+阅读 · 2019年11月25日
【电子书】Flutter实战305页PDF免费下载
专知会员服务
23+阅读 · 2019年11月7日
相关资讯
微信小程序支持webP的WebAssembly方案
前端之巅
19+阅读 · 2019年8月14日
美团:基于跨平台框架Flutter的动态化平台建设
前端之巅
14+阅读 · 2019年6月17日
2020年你应该知道的8种前端JavaScript趋势和工具
前端之巅
5+阅读 · 2019年6月9日
从webview到flutter:详解iOS中的Web开发
前端之巅
5+阅读 · 2019年3月24日
React Native 分包哪家强?看这文就够了!
程序人生
13+阅读 · 2019年1月16日
Python为啥这么牛?
Python程序员
3+阅读 · 2018年3月30日
代码这样写不止于优雅(Python版)
数说工作室
4+阅读 · 2017年7月17日
Top
微信扫码咨询专知VIP会员