[译][官方文档] Flutter/Dart 状态管理库 Riverpod - 概念 - 关于钩子

原文链接:About hooks | Riverpod

pub:riverpod | Dart Package (flutter-io.cn)

译时版本: 2.4.9


之前翻译过 Riverpod 的官方文档,现在随着版本更新,官方文档又多了很多新内容,所以再补充翻译一下。

之前翻译过的内容,现在官方文档有中文了。
Flutter状态管理库Riverpod官方文档翻译汇总 - 掘金 (juejin.cn)


关于钩子

该篇将说明什么是钩子和它们如何与 Riverpod 关联。

"钩子" 是一个独立包里的通用工具类,该包独立于 Riverpod:flutter_hooks 。 虽然 flutter_hooks 是一个完全独立的包,并且和 Riverpod 没有什么关系(至少没有直接关系),将 Riverpod 和 flutter_hooks 配对使用也是一个常用做法。

是否应该使用钩子?

钩子是一个强大的工具,但是不是对所有人。

如果是 Riverpod 的新人,很可能应该避免使用钩子。

虽然很有用,钩子对于 Riverpod 也不是必须的。

不应该是因为使用 Riverpod 而使用钩子。反而应该是想要使用钩子而使用钩子。

使用钩子是一种权衡。它们对于产出健壮或重用的代码来说是强大的,但是它们也有一些新概念需要学习,并且开始时会容易产生困扰。钩子不是核心的 Flutter 概念。因此,它们在 Flutter/Dart 没有相关内容。

钩子是什么?

钩子是在组件内部使用的函数。它们是设计作为 StatefulWidget (有状态组件)的替代方案,使业务逻辑可重用或组合。

钩子是来自于 React 的概念,[flutter_hooks] 只不过是 React 上的实现到 Flutter 的移植。

因此,是的,在 Flutter 上钩子可能感觉不太适合。理想地,将来会有一个方案来解决钩子解决的问题,会特别针对 Flutter 进行设计。

如果 Riverpod 的 provider 是用于"全局"应用状态,钩子则用于本地组件状态。钩子是典型地用于处理有状态的 UI 对象,如 TextEditingControllerAnimationController

它们也能作为"builder"模式的替代品替换组件,如FutureBuilder/TweenAnimatedBuilder ,通过不带"嵌套"的做法 - 大大提高了可读性。

通常,钩子有助于以下处理:

  • 表单
  • 动画
  • 用户事件响应
  • 。。。

作为示例,可以使用钩子手动实现一个渐入动画,即使组件以不可见开始,然后慢慢出现。

如果使用 StatefulWidget ,代码可能如下:

scala 复制代码
class FadeIn extends StatefulWidget {
  const FadeIn({Key? key, required this.child}) : super(key: key);

  final Widget child;

  @override
  State<FadeIn> createState() => _FadeInState();
}

class _FadeInState extends State<FadeIn> with SingleTickerProviderStateMixin {
  late final AnimationController animationController = AnimationController(
    vsync: this,
    duration: const Duration(seconds: 2),
  );

  @override
  void initState() {
    super.initState();
    animationController.forward();
  }

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

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: animationController,
      builder: (context, child) {
        return Opacity(
          opacity: animationController.value,
          child: widget.child,
        );
      },
    );
  }
}

使用钩子,相同的处理代码会如下:

scala 复制代码
class FadeIn extends HookWidget {
  const FadeIn({Key? key, required this.child}) : super(key: key);

  final Widget child;

  @override
  Widget build(BuildContext context) {
    // 创建 AnimationController 。该 controller 会在组件卸载后自动清除。
    final animationController = useAnimationController(
      duration: const Duration(seconds: 2),
    );

    // useEffect 等同于 initState + didUpdateWidget + dispose 。
    // 传递给 useEffect 的回调会在钩子第一次被调用时执行,然后会在列表作为第二个参数被传递时执行。
    // 因为这里传递空的常量列表,就完全等同于 `initState` 。
    useEffect(() {
      // 在组件第一次渲染时启动动画。
      animationController.forward();
      // 这里可以返回一些"清除"逻辑作为可选处理。
      return null;
    }, const []);

    // 告诉 Flutter 在动画更新时重建组件。
    // 这等同于 AnimatedBuilder
    useAnimation(animationController);

    return Opacity(
      opacity: animationController.value,
      child: child,
    );
  }
}

代码中有一些有趣的内容需要注意:

  • 没有内存泄露。即使组件重新构建,代码也不会再次创建一个新的 AnimationController ,然后 controller 会在组件卸载后正确地被释放。

  • 在相同的组件中,钩子想用多少次就能用多少次。因此,想创建多个 AnimationController 就可以创建多个。

    less 复制代码
    @override
    Widget build(BuildContext context) {
      final animationController = useAnimationController(
        duration: const Duration(seconds: 2),
      );
      final anotherController = useAnimationController(
        duration: const Duration(seconds: 2),
      );
    
      ...
    }

    这会创建两个 controller ,不会有任何不好的后果。

  • 如果需要,可以将该业务逻辑重构为一个独立可重用的函数:

    dart 复制代码
    double useFadeIn() {
      final animationController = useAnimationController(
        duration: const Duration(seconds: 2),
      );
      useEffect(() {
        animationController.forward();
        return null;
      }, const []);
      useAnimation(animationController);
      return animationController.value;
    }

    之后可以在组件内部使用该函数,只要该组件是 HookWidget

    dart 复制代码
    class FadeIn extends HookWidget {
      const FadeIn({Key? key, required this.child}) : super(key: key);
    
      final Widget child;
    
      @override
      Widget build(BuildContext context) {
        final fade = useFadeIn();
    
        return Opacity(opacity: fade, child: child);
      }
    }

    注意 useFadeIn 函数和 FadeIn 组件是完全独立的。

    如果需要,可以在一个完全不同的组件中使用 useFadeIn 函数,它仍然能正常工作!

钩子的规则

钩子有一些独有的限制:

  • 它们只能在继承 HookWidget 的组件的 build 方法中使用:

    :

    scala 复制代码
    class Example extends HookWidget {
      @override
      Widget build(BuildContext context) {
        final controller = useAnimationController();
        ...
      }
    }

    不好:

    scala 复制代码
    // 不是 HookWidget
    class Example extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        final controller = useAnimationController();
        ...
      }
    }

    不好:

    scala 复制代码
    class Example extends HookWidget {
      @override
      Widget build(BuildContext context) {
        return ElevatedButton(
          onPressed: () {
            // _actually_ 不在 "build" 方法中,但是在用户交互生命周期(这里是"on pressed")中。
            final controller = useAnimationController();
          },
          child: Text('click me'),
        );
      }
    }
  • 不能在条件分支或循环中使用。

    不好:

    scala 复制代码
    class Example extends HookWidget {
      const Example({required this.condition, super.key});
      final bool condition;
      @override
      Widget build(BuildContext context) {
        if (condition) {
          // Hooks 不应该在 "if"/"for" 中使用。。。
          final controller = useAnimationController();
        }
        ...
      }
    }

更多关于钩子的信息,查看 flutter_hooks

钩子 和 Riverpod

安装

因为钩子是独立于 Riverpod 的,所以需要单独安装钩子。如果想使用它们,安装 hooks_riverpod 就足够了。如果仍需要安装 flutter_hooks 到依赖中。查看开始使用获取更多信息。

用法

一些情况下,可能想编写同时使用钩子和 Riverpod 的组件。但是可能已经多次注意到,钩子和 Riverpod 分别提供了各自的自定义组件的基类:HookWidgetConsumerWidget

但是类一次只能继承一个父类。

要解决该问题,可以使用 hooks_riverpod 包。该包提供了一个 HookConsumerWidget 类,它将 HookWidgetConsumerWidget 绑定为了单个类型。

因此就可以编写 HookConsumerWidget 的子类来代替继承 HookWidget

dart 复制代码
// 继承 HookConsumerWidget 代替继承 HookWidget
class Example extends HookConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 在这里可以同时使用钩子和 provider
    final counter = useState(0);
    final value = ref.watch(myProvider);

    return Text('Hello $counter $value');
  }
}

另一个替代方案,可以使用两个包提供的 "builder" 。

例如,可以继续使用 StatelessWidget ,然后使用 HookBuilderConsumer

dart 复制代码
class Example extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 同时使用两个包的 builder
    return Consumer(
      builder: (context, ref, child) {
        return HookBuilder(builder: (context) {
          final counter = useState(0);
          final value = ref.watch(myProvider);

          return Text('Hello $counter $value');
        });
      },
    );
  }
}

注意

该方式无需使用 hooks_riverpod 。只需要 flutter_riverpod

如果喜欢这种方式,hooks_riverpod 会提供 HookConsumer 以提高效率,它同时绑定了两个 builder :

dart 复制代码
class Example extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 等同于同时使用 Consumer 和 HookBuilder 。
    return HookConsumer(
      builder: (context, ref, child) {
        final counter = useState(0);
        final value = ref.watch(myProvider);

        return Text('Hello $counter $value');
      },
    );
  }
}

相关推荐
江上清风山间明月1 天前
Flutter开发的应用页面非常多时如何高效管理路由
android·flutter·路由·页面管理·routes·ongenerateroute
Zsnoin能2 天前
flutter国际化、主题配置、视频播放器UI、扫码功能、水波纹问题
flutter
早起的年轻人2 天前
Flutter CupertinoNavigationBar iOS 风格导航栏的组件
flutter·ios
HappyAcmen2 天前
关于Flutter前端面试题及其答案解析
前端·flutter
coooliang2 天前
Flutter 中的单例模式
javascript·flutter·单例模式
coooliang2 天前
Flutter项目中设置安卓启动页
android·flutter
JIngles1232 天前
flutter将utf-8编码的字节序列转换为中英文字符串
java·javascript·flutter
B.-2 天前
在 Flutter 中实现文件读写
开发语言·学习·flutter·android studio·xcode
freflying11193 天前
使用jenkins构建Android+Flutter项目依赖自动升级带来兼容性问题及Jenkins构建速度慢问题解决
android·flutter·jenkins
机器瓦力3 天前
Flutter应用开发:对象存储管理图片
flutter