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 はサーバから取得するユーザデータのようなアプリケーション全体で使われる状態を指します。詳細は以下の記事を参照してください。
本記事では Flutter Hooks と Riverpod を組み合わせて使う方法についてまとめます。
各ライブラリの基礎知識は前提知識としますが、以下の記事でそれぞれ説明していますので、必要に応じて参照してください。
インストール
まず 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
を使う必要はなく、以下のように HookWidget
と Consumer
を使った実装にすることができます。
// 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"), ) ], ), ], ); }); } }