[読書会]Riverpod.dev/docs 翻訳及び補足(About hooks)

Riverpod.dev/docs(開始URL: https://riverpod.dev/docs/introduction/why_riverpod )には日本語訳ページ(/ja)も部分的には既にあるのですが、その部分はそのまま参照して、その他の英語訳のままのページについては自力で翻訳および解説を補足してみました。まだ工事中ですが最終的に満足ができたら上記サイトの翻訳ページに献上できないかな等という目標もあります。

本ブログの(上記Riverpod.dev/docsの)翻訳および解説の目次頁(構成上のトップページ)は次のURLになります。

本頁は、上記のうちの、「About hooks」の章の翻訳および解説頁になります。

About hooks

注釈)現在Riverpod.dev/docs中の本章「About hooks」には既に日本語訳ページ(https://riverpod.dev/ja/docs/concepts/about_hooks)が存在しますので、原文はこちらを参照願います。当ブログではこの日本語訳はそのまま参照して補足を追加しています。

Hooksについて

このページでは Hooks とは何か、そして Riverpod とどのように関連しているかについて説明します。

Hooks“(flutter_hooks)は Riverpod から独立し、別のパッケージで提供されるユーティリティです。

flutter_hooksは完全に別のパッケージであり、(少なくとも直接的には) Riverpod とは何の関係もありませんが、Riverpod とflutter_hooksを組み合わせて使用することが一般的です。

Hooks を使用するべきか

Hooks は強力なツールです、しかし全ての人にとって適しているわけではありません。
Riverpod を始めてつかうかたは、Hooks の使用を避けることをお勧めします。

Hooks は便利ではありますが、Riverpod には必須ではありません。
Riverpod だからと言って Hooks を使い始めるべきではありません。
Hooks を使いたいという理由で Hooks を使い始めるべきです。

Hooks を使うことはトレードオフです。 Hooks はコードを堅牢かつ再利用可能にするのに役立ちますが、新しいコンセプトを学ぶ必要があるため、
最初は混乱するかもしれません。
Hooks は Flutter のコアコンセプトではないため、Flutter/Dart で不自然に感じるかもしれません。

Hooks は何?

Hooks はウィジェット内で使用される関数です。
ロジックをより再利用可能(reusable)かつ組み合わせ(composable)可能にするためにStatefulWidgetの代替として設計されています。

Hooks は React から来た概念であり、flutter_hooksはその React 実装を Flutter に移植したものです。 そのため、Hooks は Flutter の中では多少違和感を感じるかもしれません。
理想的には、将来 Hooks が解決する問題に対して、Flutter 専用に設計された解決策が出てくることが望まれます。

Riverpod の provider は”global”アプリケーションの状態(State)を管理するためのものですが、Hooks はローカルウィジェットの状態(State)を管理するためのものです。
Hooks は通常、TextEditingControllerやAnimationControllerなどのステートフルな UI オブジェクトを扱うために使用されます。
また、「ビルダーパターン」の代替としても機能し、FutureBuilderやTweenAnimatedBuilderなどのウィジェットを”ネスト”せずに置き換えることができ、可読性を大幅に向上させます。

一般的に、Hooks は以下のような場合に役立ちます。

  • forms
  • animations
  • ユーザーイベントへの反応

例えば、ウィジェットが見えない状態から徐々に現れるフェードインアニメーションを手動で実装する場合、Hooks を使用することができます。

StatefulWidgetを使用する場合、コードは次のようになります:

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

Hooks を使用すると、同等のコードは次のようになります。

Dart
class FadeIn extends HookWidget {
  const FadeIn({Key? key, required this.child}) : super(key: key);

  final Widget child;

  @override
  Widget build(BuildContext context) {
    // AnimationController を作成します。
    // このコントローラーはウィジェットがアンマウントされると自動的に破棄されます。
    final animationController = useAnimationController(
      duration: const Duration(seconds: 2),
    );

    // useEffectは initState + didUpdateWidget + disposeに相当します。
    // useEffectに渡されたコールバックは、Hooksが最初に呼び出されたときに実行されます。
    // その後、第二引数に渡されたリストが変更されるたびに再度実行されます。
    // ここでは空のリストが渡されているため、厳密には`initState`と同じです。
    useEffect(() {
      // ウィジェットが最初に描画されたときにアニメーションを開始します。
      animationController.forward();
      // ここにオプションで"dispose"ロジックを返すことができます。
      return null;
    }, const []);

    // アニメーションが更新された時にウィジェットを再ビルドするようにFlutterに指示します。
    // これはAnimatedBuilderに相当します。
    useAnimation(animationController);

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

このコードにはいくつかの興味深い点があります:

  • メモリリークはありません。このコードはウィジェットが再ビルドされるたびに新しい AnimationController を作成しません。 代わりに、ウィジェットがアンマウントされるときにコントローラーが正しく破棄されます。
  • 同じウィジェットで Hooks を何度でも使用できます。
    そのため、必要に応じて複数の AnimationController を作成することができます。
Dart
@override
Widget build(BuildContext context) {
  final animationController = useAnimationController(
    duration: const Duration(seconds: 2),
  );
  final anotherController = useAnimationController(
    duration: const Duration(seconds: 2),
  );

  ...
}

これにより、何の悪影響もなく 2 つのコントローラが作成されます。

  • このロジックを別の再利用可能な関数にリファクタリングすることができます。
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関数を使用することができます。

Hooks のルール

フックには固有の制約があります。

1.これらは、HookWidget を拡張するウィジェットの build メソッド内でのみ使用できます。

(正しいコード)
class Example extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final controller = useAnimationController();
    ...
  }
}
(誤ったコード(その1))
// Not a HookWidget
class Example extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final controller = useAnimationController();
    ...
  }
}
(誤ったコード(その2))
class Example extends HookWidget {
  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () {
        // Not _actually_ inside the "build" method, but instead inside
        // a user interaction lifecycle (here "on pressed").
        final controller = useAnimationController();
      },
      child: Text('click me'),
    );
  }
}

2.条件付きで使ったり、ループの中で使ったりすることはできない。

(誤ったコード(その3))
class Example extends HookWidget {
  const Example({required this.condition, super.key});
  final bool condition;
  @override
  Widget build(BuildContext context) {
    if (condition) {
      // Hooks should not be used inside "if"s/"for"s, ...
      final controller = useAnimationController();
    }
    ...
  }
}

フックについての詳細は flutter_hooks を参照。

Hooks and Riverpod

Installation

フックはRiverpodから独立しているため、フックを別途インストールする必要がある。 フックを使いたい場合は、hooks_riverpodをインストールするだけでは不十分です。 依存関係にflutter_hooksを追加する必要があります。 詳しくは Getting started) を見てください。

Usage

場合によっては、フックと Riverpod の両方を使用するウィジェットを作成したい場合があります。
ただし、すでにお気づきかもしれませんが、フックと Riverpod の両方が独自のカスタム ウィジェットのベース タイプ(HookWidget と ConsumerWidget.)を提供します。

この問題を解決するには、hooks_riverpod パッケージを使用できます。このパッケージは、HookWidget と ConsumerWidget の両方を 1 つの型に結合する HookConsumerWidget クラスを提供します。
したがって、HookWidget の代わりに HookConsumerWidget をサブクラス化できます。

Dart
// We extend HookConsumerWidget instead of HookWidget
class Example extends HookConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // We can use both hooks and providers here
    final counter = useState(0);
    final value = ref.watch(myProvider);

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

あるいは、両方のパッケージで提供される「ビルダー」を使用することもできます。
たとえば、StatelessWidget の使用にこだわり、HookBuilder と Consumer の両方を使用することもできます。

Dart
class Example extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // We can use the builders provided by both packages
    return Consumer(
      builder: (context, ref, child) {
        return HookBuilder(builder: (context) {
          final counter = useState(0);
          final value = ref.watch(myProvider);

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

NOTE)
このアプローチは、hooks_riverpod を使用しなくても機能します。 flutter_riverpod のみが必要です。

このアプローチが気に入った場合は、hooks_riverpod が、両方のビルダーを 1 つに組み合わせた HookConsumer を提供することでそれを効率化します。

Dart
class Example extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Equivalent to using both Consumer and HookBuilder.
    return HookConsumer(
      builder: (context, ref, child) {
        final counter = useState(0);
        final value = ref.watch(myProvider);

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

(次の記事はこちら)

コメントを残す