【初心者向け】FlutterでStateProviderを使い状態管理をしてみよう
こんにちは、ワニかず@40歳 出戻りエンジニアです。
今回は、Flutterで状態管理を行う方法の一つとして存在する、
StateProviderについてまとめました。
はじめに
Flutter アプリケーション開発において、状態管理は非常に重要な要素です。適切な状態管理を行うことで、アプリの保守性、拡張性、パフォーマンスが向上します。今回は、Flutter における状態管理の選択肢の一つである StateProvider について詳しく解説します。
StateProvider とは
StateProvider は、Riverpod パッケージが提供する状態管理ソリューションの一つです。簡単な状態を管理するために設計されており、読み取りと書き込みの両方の操作をサポートします。特に単一の値を管理する場合に最適です。
StateProvider vs Provider
通常の Provider が読み取り専用のデータを提供するのに対し、StateProvider は状態の変更も可能にします。これにより、UI と状態の同期が容易になります。
StateProvider の基本的な使い方
1. パッケージのインストール
まず、pubspec.yaml
ファイルに Riverpod パッケージを追加します。
dependencies:
flutter:
sdk: flutter
# Riverpodパッケージをインポート - StateProviderを使用するために必要
flutter_riverpod: ^2.4.0
コードの説明:
-
flutter_riverpod: ^2.4.0
: Riverpodパッケージのバージョン2.4.0以上をインストールします。このパッケージがStateProviderを含む各種プロバイダーを提供します。
2. アプリケーションの準備
アプリケーションのルートで ProviderScope
を設定します。
import 'package:flutter/material.dart';
// Riverpodパッケージをインポート
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(
// ★重要★: ProviderScope を追加して Riverpod を有効にする
// このウィジェットがアプリ全体のProviderの状態を管理する
ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: 'StateProvider Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: HomePage(),
);
}
}
コードの説明:
-
import 'package:flutter_riverpod/flutter_riverpod.dart'
: Riverpodの機能を使用するために必要なインポート文です。 -
ProviderScope(child: MyApp())
: アプリケーションのルートにProviderScopeを配置します。これはStateProviderを含むすべてのプロバイダーの状態を管理・保持するコンテナです。このウィジェットがないとRiverpodのプロバイダーは機能しません。
3. StateProvider の定義
簡単なカウンターアプリを例に、StateProvider を定義します。
import 'package:flutter_riverpod/flutter_riverpod.dart';
// ★重要★: カウンターの値を管理する StateProvider
// グローバル変数として定義することで、アプリのどこからでもアクセス可能
final counterProvider = StateProvider<int>((ref) => 0);
コードの説明:
-
final counterProvider
: グローバル変数としてプロバイダーを定義します。これにより、アプリのどこからでもこの状態にアクセスできます。 -
StateProvider<int>
: 整数型の値を管理するStateProviderを作成します。ジェネリック型<int>
で管理する値の型を指定します。 -
((ref) => 0)
: プロバイダーの初期値を0に設定します。ref
パラメータはProviderReferenceで、他のプロバイダーを参照する際に使用できます。
4. 状態の利用と更新
StateProvider を使って状態を読み取り、更新する方法を見てみましょう。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// ★重要★: ConsumerWidgetを継承することで、このウィジェットはプロバイダーの状態を監視できる
class HomePage extends ConsumerWidget {
// ★重要★: WidgetRefパラメータを受け取り、これを通じてプロバイダーにアクセスする
Widget build(BuildContext context, WidgetRef ref) {
// ★重要★: StateProvider から状態を取得(値が変更されると自動的に再ビルド)
final count = ref.watch(counterProvider);
return Scaffold(
appBar: AppBar(
title: Text('StateProvider Demo'),
),
body: Center(
child: Text(
'$count', // 取得した状態値を表示
style: TextStyle(fontSize: 40),
),
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
// ★重要★: StateProviderの状態を更新(インクリメント)
onPressed: () => ref.read(counterProvider.notifier).state++,
tooltip: 'Increment',
child: Icon(Icons.add),
),
SizedBox(height: 10),
FloatingActionButton(
// ★重要★: StateProviderの状態を更新(デクリメント)
onPressed: () => ref.read(counterProvider.notifier).state--,
tooltip: 'Decrement',
child: Icon(Icons.remove),
),
],
),
);
}
}
コードの説明:
-
class HomePage extends ConsumerWidget
: StatelessWidgetではなくConsumerWidgetを継承します。これによりWidgetRefが提供され、プロバイダーにアクセスできるようになります。 -
Widget build(BuildContext context, WidgetRef ref)
: 通常のbuildメソッドに加え、WidgetRef ref
パラメータを受け取ります。 -
final count = ref.watch(counterProvider)
:ref.watch
メソッドでStateProviderの現在の値を監視します。値が変更されるとウィジェットが自動的に再ビルドされます。 -
ref.read(counterProvider.notifier).state++
:-
ref.read
: 値の変更を監視せず、一度だけ値を読み取ります(ボタン押下時など) -
.notifier
: StateProviderの状態を管理するStateControllerにアクセス -
.state
: 実際の状態値にアクセス -
++
: 状態値をインクリメント(同様に--
はデクリメント)
-
StateProvider の活用シナリオ
1. フォームの入力状態管理
// ★重要★: テキスト入力の状態を管理するStateProvider
// 文字列型の状態を管理し、初期値は空文字
final textInputProvider = StateProvider<String>((ref) => '');
// 使用例
TextField(
// ★重要★: テキストが変更されたときにStateProviderの値を更新
onChanged: (value) => ref.read(textInputProvider.notifier).state = value,
)
コードの説明:
-
final textInputProvider = StateProvider<String>((ref) => '')
: 文字列型の状態を管理するStateProviderを作成し、初期値を空文字列に設定します。 -
onChanged: (value) => ref.read(textInputProvider.notifier).state = value
:- TextField内でテキストが変更されるたびに、StateProviderの状態を新しい値で更新します。
-
ref.read
は値の更新時に使用し、UIの再構築を発生させません。 -
.notifier.state = value
で実際の状態値を更新します。
2. トグルスイッチの状態
// ★重要★: ダークモードの設定状態を管理するStateProvider
// 真偽値型の状態を管理し、初期値はfalse(ライトモード)
final isDarkModeProvider = StateProvider<bool>((ref) => false);
// 使用例
Switch(
// ★重要★: 現在の状態値を監視してスイッチの状態に反映
value: ref.watch(isDarkModeProvider),
// ★重要★: スイッチが切り替えられたときに状態を更新
onChanged: (value) => ref.read(isDarkModeProvider.notifier).state = value,
)
コードの説明:
-
final isDarkModeProvider = StateProvider<bool>((ref) => false)
: 真偽値型の状態を管理するStateProviderで、初期値はfalse(ライトモード)に設定します。 -
value: ref.watch(isDarkModeProvider)
:-
ref.watch
でStateProviderの現在の値を監視します。 - 値が変更されるとSwitchウィジェットが自動的に再構築され、UIに反映されます。
-
-
onChanged: (value) => ref.read(isDarkModeProvider.notifier).state = value
:- スイッチが切り替えられたときに状態を更新します。
- 新しい状態値には引数の
value
(trueまたはfalse)を使用します。
3. フィルター選択の管理
// ★重要★: 選択されたフィルターのインデックスを管理するStateProvider
// 整数型の状態を管理し、初期値は0(すべて)
final selectedFilterProvider = StateProvider<int>((ref) => 0);
// 使用例
SegmentedButton<int>(
segments: [
ButtonSegment(value: 0, label: Text('すべて')),
ButtonSegment(value: 1, label: Text('お気に入り')),
ButtonSegment(value: 2, label: Text('未完了')),
],
// ★重要★: 現在選択されているフィルターをStateProviderから取得
selected: {ref.watch(selectedFilterProvider)},
// ★重要★: 選択が変更されたときにStateProviderを更新
onSelectionChanged: (Set<int> selection) {
ref.read(selectedFilterProvider.notifier).state = selection.first;
},
)
コードの説明:
-
final selectedFilterProvider = StateProvider<int>((ref) => 0)
: 整数型のStateProviderで、フィルターのインデックスを管理します。初期値は0(「すべて」フィルター)です。 -
selected: {ref.watch(selectedFilterProvider)}
:-
ref.watch
でStateProviderの値を監視します。 - SegmentedButtonウィジェットは選択されたセグメントをSetとして受け取るため、波括弧
{}
で囲んでSetに変換しています。 - 状態が変更されると自動的に再ビルドされ、選択状態が更新されます。
-
-
onSelectionChanged: (Set<int> selection) { ... }
:- ユーザーが別のセグメントを選択したとき、StateProviderの状態を更新します。
-
selection.first
で選択されたセグメントの値(0, 1, または2)を取得します。
## StateProvider の制限と注意点
1. **単一の値の管理**: StateProvider は単一の値を管理するのに適しています。複雑なオブジェクトや状態の場合は、StateNotifier や Notifier を検討してください。
2. **パフォーマンスへの配慮**: 頻繁に更新される状態の場合、再ビルドの範囲を最小限に抑えるために、ConsumerWidget の代わりに Consumer ウィジェットを使用することを検討してください。
3. **クラスや複雑なオブジェクトの使用**: StateProvider でクラスを使用する場合、イミュータブルなオブジェクトを使用するか、状態の変更時に新しいインスタンスを作成する必要があります。
```dart
// 例:ユーザー情報を管理するStateProvider
final userProvider = StateProvider<User>((ref) => User());
// ★注意★: 良くない例 - オブジェクトの内部プロパティを直接変更
ref.read(userProvider.notifier).state.name = 'John'; // この変更はRiverpodに検出されない
// ★推奨★: 良い例 - 新しいインスタンスを作成して状態を更新
ref.read(userProvider.notifier).state = User(name: 'John'); // Riverpodが変更を検出できる
コードの説明:
-
良くない例
:-
ref.read(userProvider.notifier).state.name = 'John'
:- Userオブジェクトの内部プロパティを直接変更しています。
- 重要な問題点: この変更はStateProviderによって検出されません。オブジェクトの参照自体は変わっていないためです。
- 結果として、UIは再ビルドされず、変更が画面に反映されません。
-
-
良い例
:-
ref.read(userProvider.notifier).state = User(name: 'John')
:- 新しいUserオブジェクトを作成し、stateプロパティを完全に置き換えています。
- 重要なポイント: この方法では、StateProviderは状態の変更を検出でき、依存するウィジェットを適切に再ビルドします。
- イミュータブルなアプローチを取ることで、状態管理が予測可能になります。
-
-
代替アプローチ:
- 複雑なオブジェクトを扱う場合は、StateProviderではなくStateNotifierProviderの使用を検討してください。
- または、
.update()
メソッドと複製を組み合わせることもできます:// 更新メソッドを使った例 ref.read(userProvider.notifier).update((state) => User( id: state.id, // 既存のIDを保持 name: 'John', // 名前を更新 email: state.email // 既存のメールを保持 ) );
Riverpod 2.0 以降の変更点
Riverpod 2.0 からは、.state
プロパティを使用する代わりに、.update()
メソッドを使用することが推奨されています。これにより、コードの意図がより明確になります。
// 従来の方法(非推奨)
ref.read(counterProvider.notifier).state++;
// ★重要★: 推奨される方法 - updateメソッドを使用
ref.read(counterProvider.notifier).update((state) => state + 1);
コードの説明:
-
ref.read(counterProvider.notifier).state++
:- 従来の方法では、
.state
プロパティに直接アクセスして変更します。 - この方法は動作しますが、複雑な状態更新では意図が明確でなくなる可能性があります。
- 従来の方法では、
-
ref.read(counterProvider.notifier).update((state) => state + 1)
:-
.update()
メソッドは、現在の状態を引数として受け取るコールバック関数を使用します。 - コールバック関数は新しい状態値を返し、それが自動的に状態として設定されます。
- この方法は関数型プログラミングの原則に沿っており、特に複雑な状態更新で意図が明確になります。
- 複数の更新を連鎖させる場合や条件付き更新に特に有用です。
-
StateProvider と他の状態管理ソリューションの比較
ソリューション | 用途 | 複雑さ |
---|---|---|
StateProvider | 単一の値の管理 | 低 |
StateNotifierProvider | 複雑な状態の管理 | 中 |
NotifierProvider | 複雑な状態とビジネスロジック | 中〜高 |
AsyncNotifierProvider | 非同期操作を含む状態管理 | 高 |
おしまいに
光回線の「プロバイダー」とか、
「プロバイダー」って色んな意味で使われるから
「プロバイダーって言っても色んな意味があるんだよ」
と、まずは教えてよと、エンジニア1年目に思ったことがあったとさ(遠い目)
Discussion