[Flutter翻译]Flutter Hooks,告别StatefulWidget,减少模板代码。

1,412 阅读6分钟

原文地址:medium.com/flutter-com…

原文作者:medium.com/@raveeshaga…

发布时间:2019年1月5日 - 5分钟阅读

Flutter hooks已经有一段时间了,但从那时起,它们并没有得到很多人的喜爱和关注。我很想知道为什么,因为它们很棒!

在这篇文章中,我将尝试告诉你如何减少锅炉模板,并基本上删除你今天使用的所有StatefulWidget,同时也告诉你钩子是如何简单而又酷的使用!

首先,什么是钩子,钩子是怎么来的?有人知道吗? 好吧,它们最初来自于React(参见medium.com/@dan_abramo…!

钩子是一种与多个widget共享相同代码的方式,这些代码通常是重复的,或者很难在有状态的widget之间共享。我对它们的描述是 "钩子就是UI逻辑管理"。


我给大家介绍一下我在我的应用中使用最多的钩子和它们对应的stateful widgets,让大家对比一下两者,看看到底有什么收获。

Memoized钩子:

这个钩子是在widget的生命周期中缓存一个对象实例的简单方法。在创建你的BLoC、MobX商店或屏幕的通知器对象时相当方便。 这里是有状态的widget版本:

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();
  }
}

现在是钩子版本。

class MyHomePage extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final store = useMemoized(() => MyStore());
    return Container();
  }
}

这两个例子都是在做同样的工作,只是在widget的生命周期内创建一个MyStore的实例。

这里的收益并不大,但一般来说,你想初始化你的对象来加载数据,例如,Hooks也为你提供了保障。现在我们来看看useEffect吧!

Effect钩子:

就像我说的,我们要加载数据,为了做到这一点,我们通常会调用initState的方法。

有状态的widget版本:

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();
  }
}

钩子版本:

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 []的意思是,在widget没有被处置之前,不要调用效果。你可以提供一个参数数组,当一个参数发生变化时,效果就会被调用。

让我们再看一个有动画的例子!

动画钩子:

这是一个简单的例子,当一个按钮被点击时,如何旋转一个盒子。

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),
            ),
          ),
        ],
      ),
    );
  }
}

基本的旋转动画与有状态的widget

而这里相当于钩子:

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),
            ),
          ),
        ],
      ),
    );
  }
}

我们可以看到,钩子为我们管理控制器的生命周期,不需要处理它,不需要像有状态部件那样提供行情提供者。

钩子允许你创建你自己的钩子,这意味着如果你没有找到内置的东西,只需要创建你的钩子。

让我们看看如何创建一个钩子来管理TabController。

自定义钩子:

flutter_hooks包提供了两种方法来做自定义钩子,通过简单的使用函数或者创建一个自定义类。

首先,让我们看看用函数实现的自定义钩子:

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提供者由一个现有的hook来处理,叫做useSingleTickerProvider。这个很简单,当我们的自定义钩子将被使用时,长度和初始索引都必须被提供。

你看,一组被传递给useSingleTickerProvider。这是为了确保每当改变任何一个键时,行情提供者都会重新创建。例如,如果tab的数量发生了变化,我们需要缓存TabController。

我们需要缓存TabController,以便在widget的生命周期中拥有一次,这就是为什么我们要useMemoized。在这里,我们将tickerProvider作为第二个参数传递给我们,以便在ticker发生变化时重新创建控制器,即当长度initialIndex更新时。同样,这一切都是自动的!

为了处置TabController,正如我们前面看到的,我们依靠**useEffect()**函数来返回控制器的处置方法。

注意,如果提供一个新的TabController作为第二个参数,该方法也会被调用。

自定义钩子类呢?

由于钩子函数非常容易使用,我还不需要把它作为一个类来实现,但让我们看看如何做。

每当你的钩子的复杂性增加时,你应该把它作为一个类来实现,事实上,包的文档也推荐这样做。

这里是我们的TabController钩子作为一个自定义类:

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;
  }
}

你可以看到钩子的工作方式就像一个有状态的部件!你有一个状态类,一个HookState类,它可以访问你的自定义Hook类的字段(这里是hook.length)。你有一个状态类,一个HookState类,它可以访问你的自定义Hook类的字段(这里是hook.length)。而hookState的build方法则构建了你的hook的结果。所以还是很容易做到的!


钩子提供的不仅仅是这些快捷方式。例如,它可以通过管理FocusNode或TextEditingController来帮助你处理表单。跳到官方文档中去阅读更多的内容。

我喜欢钩子,并在我所有的项目中使用它们。我通常将它们与Provider和MobX搭配使用。

你可以在pub上找到hooks,pub.dev/packages/fl…

如果你想知道更多关于钩子的信息,或者如何将它们与其他流行的包(如Provider和MobX)一起使用,请在评论中告诉我。 回头见,伙计们

www.twitter.com/FlutterComm


通过( www.DeepL.com/Translator )(免费版)翻译