[译][官方文档] 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');
      },
    );
  }
}

相关推荐
Summer不秃2 小时前
Flutter之使用mqtt进行连接和信息传输的使用案例
前端·flutter
旭日猎鹰2 小时前
Flutter踩坑记录(二)-- GestureDetector+Expanded点击无效果
前端·javascript·flutter
sunly_2 小时前
Flutter:AnimatedSwitcher当子元素改变时,触发动画
flutter
AiFlutter2 小时前
Flutter封装Coap
flutter
旭日猎鹰8 小时前
Flutter踩坑记录(三)-- 更改入口执行文件
flutter
旭日猎鹰8 小时前
Flutter踩坑记录(一)debug运行生成的项目,不能手动点击运行
flutter
️ 邪神8 小时前
【Android、IOS、Flutter、鸿蒙、ReactNative 】自定义View
flutter·ios·鸿蒙·reactnative·anroid
比格丽巴格丽抱20 小时前
flutter项目苹果编译运行打包上线
flutter·ios
SoaringHeart20 小时前
Flutter进阶:基于 MLKit 的 OCR 文字识别
前端·flutter
AiFlutter1 天前
Flutter通过 Coap发送组播
flutter