投稿者について
2024/09からFlutter開発に携わっているモバイルエンジニアです。記事内容について誤りがありましたら、ご指摘いただけると幸いです。
はじめに
自分が携わっているFlutterプロジェクトではRiverpod
のStateNotifierProvider
をよく使用するのですが、「そもそもProviderって何だろう?」と思ったのが、本記事を書くきっかけとなりました。
本記事は、StateNotifierProvider
の基礎となるProvider
について深掘りし、自分なりに整理した内容となっています。
状態管理って何?
状態管理の基本的な考え方
Flutterでいう「状態」とは、アプリ内で変化するデータやUIの状態を指します。例えば、次のようなものが挙げられます。
- ボタンを押した回数(カウンター)
- ログインしているユーザーの情報
- APIから取得したデータ
- フォームの入力値
これらの状態を適切に管理することが「状態管理」です。
Flutterでは、UI(ウィジェットツリー)と状態が密接に結びついているため、状態が変化するとUIも再描画されます。
2つの状態管理の方法
状態管理の中でも大きく分けて2種類あります。
(1) ウィジェットの中だけで保持する状態
(2) アプリ全体で保持する状態
(1) の場合、画面を閉じたり別のページに移動したりして、そのページがウィジェットツリーから削除されたら状態はリセットされます。
(2) の場合、アプリ全体で持っているので、画面遷移が起きても毎回リセットされることはありません。
言い換えれば、管理される状態は主に
(1)ウィジェットに依存する状態
(2)ウィジェットに依存しない状態
の2つになると考えられます。
Providerって何?
Providerの概要
公式ドキュメント:provider | Flutter package
Provider は、Flutterの状態管理パッケージの1つです。
先ほど2種類の状態管理について説明しましたが、Providerは (2)ウィジェットに依存しない状態管理 に適しています。詳しく解説する前に、まず使い方を見ていきましょう。
Providerの使用手順
Providerは、以下の3つの手順で使用することができます。
-
状態クラスを作成
→状態のデータと状態を操作する処理を作成 -
状態管理を行う範囲にプロバイダーを設定
ウィジェットツリーの中でデータを必要とする部分に提供 -
状態の変化を通知
状態が変更された場合に、それを監視しているウィジェットを再描画
実際に使用してみよう
必要なパッケージをインストール
まずは provider
パッケージをプロジェクトに追加します。
flutter pub add provider
以下の例では、カウンターアプリを作成します。
1. 状態クラスを作成
ChangeNotifier を継承した状態クラスを定義します。
import 'package:flutter/material.dart';
/// カウントを管理するクラス
class Counter extends ChangeNotifier {
int count = 0;
void increment() {
count++;
notifyListeners(); // 状態の変更を通知
}
}
2. Providerをセットアップ
main.dart にProviderを設定します。
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'counter.dart';
import 'counter_screen.dart';
void main() {
runApp(
/// Providerを設定
ChangeNotifierProvider(
create: (_) => Counter(),
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: CounterScreen(),
);
}
}
3. 状態を利用するウィジェット
Consumer を使って状態を監視し、UIを更新します。
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'counter.dart';
/// カウンター画面
class CounterScreen extends StatelessWidget {
const CounterScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text(
/// カウント数を表示する
'${context.watch<Counter>().count}',
),
),
/// ボタン
floatingActionButton: FloatingActionButton(
/// ボタン押下時にカウントする
onPressed: () => context.read<Counter>().increment(),
child: const Icon(Icons.add),
),
);
}
}
一個一個見てみよう
1. 状態クラス
import 'package:flutter/material.dart';
/// カウントを管理するクラス
class Counter extends ChangeNotifier {
int count = 0;
void increment() {
count++;
notifyListeners(); // 状態の変更を通知
}
}
ChangeNotifierクラスとは
公式ドキュメント:ChangeNotifier class
このクラスはProviderのパッケージ内にあるクラスではなく、flutterのmaterialパッケージに存在するクラスです。
主に以下2つの操作が可能です。
-
リスナーを登録・解除する
他のウィジェットやオブジェクトが、このクラスの状態変更を監視できるようにする(addListener
,removeListner
) -
状態の変更を通知する
notifyListeners()
メソッドを呼び出すことで、登録されたリスナーに状態変更を通知する
notifyListeners()
は状態クラスのコード内にもありますね。カウントした結果を通知してくれる役割を持っています。
ちょっと深掘り
1. リスナーって何?
-
StatefulWidget
の状態や他のオブジェクトで、特定のイベントが発生したときに再描画や再計算などを実行するためのコールバックだよ! -
ChangeNotifier
クラスには、リスナーを格納する内部リストがあって、このリストは、addListener
を使って登録されたリスナーを保持して、removeListener
で削除されるよ!
2. 通知って具体的に何?
notifyListeners()
が呼ばれると、以下の流れで通知が行われるよ!
- 内部リストに登録されているリスナーを順番に取得
- 各リスナー関数を実行
2. Providerのセットアップ
import 'package:flutter/material.dart';
/// カウントを管理するクラス
class Counter extends ChangeNotifier {
int count = 0;
void increment() {
count++;
notifyListeners(); // 状態の変更を通知
}
}
2. Providerをセットアップ
main.dart にProviderを設定します。
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'counter.dart';
import 'counter_screen.dart';
void main() {
runApp(
/// Providerを設定
ChangeNotifierProvider(
create: (_) => Counter(),
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: CounterScreen(),
);
}
}
ChangeNotifierProviderとは
公式ドキュメント:ChangeNotifierProvider<T extends ChangeNotifier?> class
ここからproviderパッケージのクラスになります。
ChangeNotifierProvider
は、上記の通り、ChangeNotifierクラスの拡張クラスです。
runApp
> ChangeNotifierProvider
> MyApp
の順で設置されており、MyApp全体の状態変化イベントを監視していることが分かります。
3. 状態を利用するウィジェット
Consumer を使って状態を監視し、UIを更新します。
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'counter.dart';
/// カウンター画面
class CounterScreen extends StatelessWidget {
const CounterScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text(
/// カウント数を表示する
'${context.watch<Counter>().count}',
),
),
/// ボタン
floatingActionButton: FloatingActionButton(
/// ボタン押下時にカウントする
onPressed: () => context.read<Counter>().increment(),
child: const Icon(Icons.add),
),
);
}
}
注目するのは以下2つです。いずれもProvider
パッケージのクラスになります。
Riverpod
でもかなりお世話になっているメソッドなので、本記事で一番重要な内容かもしれません。
/// カウント数を表示する
context.watch<Counter>().count
/// ボタン押下時にカウントする
onPressed: () => context.read<Counter>().increment()
context.watchとは
Provider
から値を取得し、その値が変更されるとウィジェットを自動的に再ビルドするメソッドです。
今回の場合でいうと、カウンターの数が変化したら自動的に表示が変わる=ウィジェットが再ビルドされているということになります。
context.readとは
Provider
から値を取得し、取得した値の変更によってウィジェットが再ビルドされない(context.watchと逆)メソッドです。
カウント数を加算する際、カウンタークラスのデータは取得したいけどウィジェットの再ビルドは必要ないのでreadを使用しているということになります。
(ついでに説明)context.select
オブジェクト全体ではなく、その一部にのみ依存する場合に使用するのがcontext.select
です。
Widget build(BuildContext context) {
final name = context.select((Person person) => person.name);
return Text(name);
}
context.watch
は値全体を監視してウィジェットを再ビルドするのに対し、context.select
は指定された部分 (selector で取得した部分) の変更にのみ反応します。
余談
複数のProviderを使用する場合
先ほどのコードだと一個のProviderしか使用できない構成でした。
もし複数Providerをセットする場合はMultiProvider
を使用します。
公式ドキュメント:MultiProvider class - provider library - Dart API
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => Counter()),
],
child: const MyApp(),
),
);
}
Consumerクラス
公式ドキュメント:Consumer class - provider library - Dart API
context.watch<Counter>~
の部分ですが、以下の書き方でもできるそうです。
Consumer<Counter>(
builder: (context, counter, child) {
/// カウント時、カウント数が自動的に変化する
return CenterText('カウント: ${counter.count}'),
);
},
),
Consumer
クラスは、Provider
を使用してデータを管理する際に、特定のウィジェットの再構築を制御するWidgetクラスです。
-
Provider
=提供者=データを管理し提供するクラス -
Consumer
=消費者=データを受け取り使用するクラス
という意味合いでクラス名が使われていそうです。
ConsumerWidget
やHookConsumerWidget
などもここから取ってきていそうですね。
まとめ
普段何気なくRiverpod
を使用していますが、Provider
を基に構築されたクラスやメソッドが多く、Provider
の理解がRiverpod
の理解にも繋がると感じました。
本記事が、Provider
の理解や状態管理の入門としてお役に立てば幸いです。