【Flutter】Riverpod と Flutter Hooks を組み合わせて使う方法とその際の使い分け

FlutterでRiverpod と Flutter Hooks を組み合わせて使う方法についてまとめました。

riverpod 2.5.1
flutter_hooks 0.20.5
hooks_riverpod 2.5.1

はじめに

Flutter Hooks と Riverpodは共に状態管理のためのパッケージですがそれぞれ用途が異なります。

組み合わせて使う場合には、Flutter Hooks は Ephemeral State (Local State) を管理するために使用され、Riverpod は App State を管理するために使用されます。
Ephemeral State はチェックボックスのON/OFF状態など一つのウィジェット内で管理される状態で、App State はサーバから取得するユーザデータのようなアプリケーション全体で使われる状態を指します。詳細は以下の記事を参照してください。

docs.flutter.dev

本記事では Flutter Hooks と Riverpod を組み合わせて使う方法についてまとめます。

各ライブラリの基礎知識は前提知識としますが、以下の記事でそれぞれ説明していますので、必要に応じて参照してください。

light11.hatenadiary.com

light11.hatenadiary.com

インストール

まず hooks_riverpod パッケージをインストールします。
Terminal から追加する場合は以下のようにします。

 flutter pub add hooks_riverpod

もし Riverpod (fluter_riverpod) や Flutter Hook をまだインストールしていない場合、上記のパッケージに依存しているので一緒にインストールされます。
riverpod_annotation などは依存関係には含まれないので、必要であれば別途インストールしてください。

Flutter Hooksを使ったアプリケーションを作る

それではまず説明のために、Flutter Hooksを使った簡単なアプリケーションを作ります。

// hooks_main
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

void main() {
  runApp(const MaterialApp(home: HomePage()));
}

class HomePage extends HookWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    final countByHooks = useState(0);

    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text(countByHooks.value.toString()),
        TextButton(
          onPressed: () => countByHooks.value++,
          child: const Text("Increment Hooks"),
        )
      ],
    );
  }
}

int型の値を表示し、インクリメントするボタンを表示するだけのシンプルなアプリケーションです。
実行結果は下図のとおりです。

実行結果

Riverpodを使ったアプリケーションを作る

次にRiverpodを使って簡単なアプリケーションを作ります。
まず以下の通り、int型の状態を保持してそれをインクリメントするメソッドを持つだけの簡単なプロバイダーを作ります。

// count.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'count.g.dart';

@riverpod
class Count extends _$Count {
  @override
  int build() {
    return 0;
  }

  void increment() {
    state++;
  }
}

コード生成機能を使っているので以下のコマンドで生成します。

$ dart run build_runner build

最後にこれを使ったアプリケーションを作ります。

// riverpod_main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'count.dart';

void main() {
  runApp(
    ProviderScope(child: MyApp()),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(home: Home());
  }
}

class Home extends ConsumerWidget {
  const Home({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final countByRiverpod = ref.watch(countProvider);
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text('$countByRiverpod'),
        TextButton(
          onPressed: () => ref.read(countProvider.notifier).increment(),
          child: const Text("Increment Riverpod"),
        )
      ],
    );
  }
}

実行結果は下図のとおりです。

実行結果

Flutter HooksとRiverpodを組み合わせる

それでは次に Flutter Hooks と Riverpod を組み合わせて使用します。

ここまで示したとおり、Flutter Hooks を使うウィジェットHookWidget を継承する必要があり、Riverpod を使うウィジェットConsumerWidget を継承する必要があります。
これらを組み合わせて使う場合には、hooks_riverpod パッケージに定義されている HookConsumerWidget を継承します。

// main.dart
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

import 'count.dart';

void main() {
  runApp(
    ProviderScope(child: MyApp()),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(home: Home());
  }
}

// HookConsumerWidgetを継承したクラスを作る(HookWidgetとConsumerWidgetの両方の機能を使える)
class Home extends HookConsumerWidget {
  const Home({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // Flutter Hooksによる状態(Local State)を取得
    final countByHooks = useState(0);
    // Riverpodによる状態(App State)を取得
    final countByRiverpod = ref.watch(countProvider);

    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(countByHooks.value.toString()),
            TextButton(
              onPressed: () => countByHooks.value++,
              child: const Text("Increment Hooks"),
            )
          ],
        ),
        Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('$countByRiverpod'),
            TextButton(
              onPressed: () => ref.read(countProvider.notifier).increment(),
              child: const Text("Increment Riverpod"),
            )
          ],
        ),
      ],
    );
  }
}

これを実行すると下図の結果が得られます。

実行結果

Consumerを使うケース

Riverpod は ConsumerWidget の代わりに Consumer を使うこともできます。
これを使う場合には HookConsumerWidget を使う必要はなく、以下のように HookWidgetConsumer を使った実装にすることができます。

// main.dart
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

import 'count.dart';

void main() {
  runApp(
    ProviderScope(child: MyApp()),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(home: Home());
  }
}

// ConsumerWidgetではなくConsumerを使う場合にはHookWidgetでOK
class Home extends HookWidget {
  const Home({super.key});

  @override
  Widget build(BuildContext context) {
    // Flutter Hooksによる状態(Local State)を取得
    final countByHooks = useState(0);

    return Consumer(builder: (context, ref, _) {
      // Riverpodによる状態(App State)を取得
      final countByRiverpod = ref.watch(countProvider);

      return Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text(countByHooks.value.toString()),
              TextButton(
                onPressed: () => countByHooks.value++,
                child: const Text("Increment Hooks"),
              )
            ],
          ),
          Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text('$countByRiverpod'),
              TextButton(
                onPressed: () => ref.read(countProvider.notifier).increment(),
                child: const Text("Increment Riverpod"),
              )
            ],
          ),
        ],
      );
    });
  }
}

関連

light11.hatenadiary.com

light11.hatenadiary.com

参考

riverpod.dev