はじめに
最近趣味の範囲でモバイルアプリ開発向けのフレームワークのFlutterを勉強しております。非常に分かりやすいフレームワークであるものの、一部の領域において、他のフレームワーク(特にReact)を触った経験のあるなしが、理解のスピードに大きく依存すると感じ、基本的には自身の頭の整理のためですが、そちらも兼ねて記録を残すことにしました。
※ちなみにReactも趣味の範囲です。。
Flutterおさらい
Flutterでは画面に映るものは全てWidgetと呼び、画面内のボタン、ヘッダー、文章も全てWidgetと呼びます。LINEアプリで表現すると、ともだちの表示名も、ユーザーのアイコンも、メッセージも全てWidgetと呼び、Flutterで作るアプリは全てWidgetと呼ばれる部品から構成されています。
ざっくり表すとLINEアプリはFlutterでは下記のように構成され、アプリ自体のWidgetとなる「アプリWidget」の中に各画面のWidgetが存在し、例えば「トーク画面Widget」の中には「ヘッダーWidget」「チャットエリアWidget」があり、、と続きます。最終的には一つのボタン、文章といったレベルまでWidgetが続くわけです。
このWidgetには、Flutter公式曰く大きく2種類あり、言葉から説明すると「Stateful Widget」と「Stateless Widget」の二つで、直訳すると「状態を持つWidget」と「状態を持たないWidget」となります。ここでいう「状態」をもう少し長く説明すると時間が経つにつれて変化する可能性のある情報といったりもします。
StatefulWidget vs StatelessWidget
ここで一度LINEアプリの場合の具体例を挙げてみます。
StatelessWidget(状態を持たない)
- メッセージ: 一度送信したメッセージは編集できないため、表示内容が変わることはない
- 戻るボタン: 常に同じ見た目で、内容が変化しない
- 電話ボタン: ずっと同じ形をしている
StatefulWidget(状態を持つ)
- ユーザーアイコン: 友達がアイコン画像を変更すると更新される
- 未読バッジ: 新着メッセージの数によって数字が変わる
- オンライン状態: ユーザーがオンライン/オフラインで表示が変わる
StatefulWidgetの課題
Stateful Widgetは自身の箱の中にStateを保管していますが、基本的に自身の中にしまっているため、他のWidgetは中身を見るのに苦労してしまいます。
具体的な問題例
シナリオ: あるユーザーが自身のアイコン画像を変更した場合
- トーク画面のアイコンは更新される
- でも友達一覧画面のアイコンは古いまま...
これでは一貫性のないUIになってしまいます。
なぜ共有が難しいのか?
Widgetの階層を考えてみましょう:
アプリ
├── 友達一覧画面
│ └── アイコン(A)
└── トーク画面
└── チャットエリア
└── アイコン(B)
アイコン(A)がアイコン(B)の情報を取得しようとすると:
アイコンA → 友達一覧画面 → アプリ → トーク画面 → チャットエリア → アイコンB
とても遠回りで、複雑ですよね...
解決策(Providerの登場)
ここで初めて状態管理ライブラリの出番です。Flutterには「Provider」と「Riverpod」と二つ解決策がありますが、まずは先に登場したProviderから見ていきましょう。
Providerとは?
Providerは**「状態を中央管理する仕組み」**です。各Widgetが個別に状態を持つのではなく、共通の場所に状態を置いて、みんなでアクセスできるようにします。
Providerのメリット
- 一箇所で状態管理: 同じ情報を複数の場所で使える
- 自動更新: 状態が変わると、関連するWidget全てが自動で更新
- シンプルなアクセス: 複雑な階層を辿る必要がない
Providerの重要な概念
Providerを理解するために、まず4つの重要な概念を押さえましょう:
ChangeNotifier(変更通知者)
「状態(データ)を持ち、変化を通知できるクラス」
- 状態(State)を内部に保持
- 状態が変わったら
notifyListeners()
で監視者に通知 - 継承して使用する
ChangeNotifierProvider(変更通知提供者)
「ChangeNotifierをラッパーとして提供するWidget」
- ChangeNotifierのインスタンスを作成
- ラッパーとして機能:この中に配置した全てのWidgetが同じデータにアクセス可能
- アプリの上位階層に配置して、下位のWidgetから参照できるようにする
Consumer(消費者)
「状態の変化を監視して、自動で画面を更新するWidget」
- 状態の変化を常に監視
- 変化があったら自動で画面を再描画
- 必ずbuilderプロパティを持つ(これが「変化したときの処理」を定義)
-
Consumer<監視したい型>(builder: (context, 最新データ, child) => Widget)
という形
notifyListeners(リスナーに通知)
「変化したことを全員に一斉通知するメソッド」
- 状態が変わった時に呼び出す
- 監視している全てのConsumerに「変わったよ」と通知
コード例で理解するProvider
ちょっと長いですが...見て欲しいです。
まず、pubspec.yaml
にproviderライブラリを追加:
dependencies:
flutter:
sdk: flutter
provider: ^6.1.1 # ←この行を追加
次に、Dartファイルでインポート:
// 必要なライブラリをインポート
import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; // ←Providerを使うために必須!
// 1. LINEユーザーのプロフィール情報を管理するクラス
// ↓ ChangeNotifierを継承 = 状態を持ち、変更を通知できるクラスになる
class LineUserProfile extends ChangeNotifier {
// 状態(State)をプライベート変数として保持
String _profileImageUrl = 'https://example.com/default_profile.png';
String _displayName = 'ユーザー名';
// ゲッター:他のWidgetがアクセスできるように
String get profileImageUrl => _profileImageUrl;
String get displayName => _displayName;
// プロフィール画像を更新するメソッド
void updateProfileImage(String newImageUrl) {
_profileImageUrl = newImageUrl;
notifyListeners(); // ←★ ここが重要。全てのConsumerに「変わったよ」と通知
}
}
// 2. アプリ全体でLINEユーザー情報を提供
class LineApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// ↓ ChangeNotifierProvider = ラッパーとしてLineUserProfileを提供
return ChangeNotifierProvider(
create: (context) => LineUserProfile(), // ←LineUserProfileのインスタンスを作成
child: MaterialApp(
home: FriendListPage(), // この下にある全てのWidgetからアクセス可能
),
);
}
}
// 3. LINEのプロフィールアイコンWidget(友達一覧でもトーク画面でも使用可能)
class LineProfileIcon extends StatelessWidget {
@override
Widget build(BuildContext context) {
// ↓ Consumer = 状態の変化を監視し、変化時に自動で再描画するWidget
return Consumer<LineUserProfile>(
// ↓ builderは必須。変化したときの処理を定義
builder: (context, profile, child) {
// profileは最新のLineUserProfile情報
return CircleAvatar(
radius: 25,
backgroundImage: NetworkImage(profile.profileImageUrl),
child: profile.profileImageUrl.isEmpty
? Icon(Icons.person, color: Colors.grey) // デフォルトアイコン
: null,
);
},
);
}
}
// 4. 実際の使用例:友達一覧での表示
class FriendListItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListTile(
leading: LineProfileIcon(), // ←Consumer内でプロフィール画像を監視
title: Consumer<LineUserProfile>( // ←表示名も監視
builder: (context, profile, child) {
return Text(profile.displayName); // 名前が変わったら自動更新
},
),
subtitle: Text('最後のメッセージ...'),
);
}
}
// 5. トーク画面でも同じアイコンを使用
class ChatMessage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
children: [
LineProfileIcon(), // ←同じConsumerを使用。自動で同期される
Expanded(
child: Container(
margin: EdgeInsets.only(left: 8),
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(12),
),
child: Text('メッセージ内容'),
),
),
],
);
}
}
// ポイント:updateProfileImage()を呼ぶと...
// 1. notifyListeners()が実行される
// 2. 全てのConsumer<LineUserProfile>に通知が行く
// 3. 友達一覧のアイコン、トーク画面のアイコン、表示名が一斉に更新
Providerの限界
ProviderはUIの一貫性を実現する素晴らしい解決策でしたが、いくつかの課題もありました:
- ボイラープレート: 書くコードが多い
- 複雑な依存関係: 大きなアプリでは管理が大変
- 型安全性: 実行時エラーの可能性
- テストの難しさ: モックの作成が複雑
Riverpod:Providerの進化形
これらの課題を解決するために登場したのがRiverpodです!
Riverpodとは?
Riverpodは**「Provider 2.0」**とも呼ばれ、Providerの作者が全く新しく設計した状態管理ライブラリです。
Riverpodの主要概念
Riverpodを理解するために、まず重要な概念を整理しましょう:
Provider(プロバイダー) ※注意:状態管理ライブラリのProviderとは別物
値や状態を提供する「部品」
Riverpodでは、用途に応じて複数種類のProviderがあります:
// 1. Provider:変わらない値を提供(設定値など)
final appNameProvider = Provider<String>((ref) => 'LINE');
// 2. StateProvider:シンプルな状態を提供(bool、int、Stringなど)
final isOnlineProvider = StateProvider<bool>((ref) => false);
// 3. StateNotifierProvider:複雑な状態とその操作を提供(今回のメイン)
final lineUserProvider = StateNotifierProvider<LineUserNotifier, LineUser>((ref) {
return LineUserNotifier();
});
// ↑この記法の意味を詳しく説明:
// StateNotifierProvider<管理者の型, 状態の型>
// 第1型パラメータ「LineUserNotifier」= 状態を管理するクラス
// - StateNotifier(Providerで言うChangeNotifier)を継承したクラス
// - 状態を変更するメソッド(updateProfileImage など)を持つ
// - 「状態の管理者」の役割
// 第2型パラメータ「LineUser」= 状態の実際のデータの型
// - 実際に保存するデータの形を定義
// - プロフィール画像URL、表示名などを持つ
// - 「管理される状態」の役割
// 関係性:
// LineUserNotifier(管理者)が LineUser(データ)を管理する
重要:この「Provider」は、先ほど学習した状態管理ライブラリの「Provider」とは全く別のものです。
状態管理ライブラリのProvider | RiverpodのProvider | |
---|---|---|
正体 | パッケージ全体の名前 | Riverpodの中の部品の種類 |
役割 | 状態管理の仕組み全体 | 値や状態を提供する部品 |
例 | ChangeNotifierProvider |
Provider , StateProvider , StateNotifierProvider
|
簡単に言うと:
- Providerパッケージ = 状態管理ライブラリの名前
- RiverpodのProvider = Riverpodで使う部品の名前
同じ「Provider」という単語でも、文脈によって意味が違うということです。
ProviderScope
Riverpodの管理領域を定義するWidget
- アプリ全体をラップして、Riverpodが動作する範囲を決める
- ProviderパッケージのChangeNotifierProviderと同じような役割
ref
プロバイダーにアクセスするための「リモコン」
-
ref.watch()
: 状態を監視(変化したら再描画) -
ref.read()
: 一回だけ値を取得(監視しない) -
ref.listen()
: 状態変化を検知してアクションを実行
ConsumerWidget
refを使えるWidget
- StatelessWidgetの代わりに使用
- buildメソッドに自動でWidgetRef(リモコン)が渡される
- 状態の変化をリモコンで監視して、変化したら自動で再描画する
※ProviderのConsumerと役割は一緒
Riverpodの改善点
1. 型安全性の向上
実行時エラーを防いで、開発中にミスが分かるように
Providerの問題例:
// 間違った型を指定(LineUserProfileが存在しない場合)
Consumer<LineUserProfile>(builder: (context, profile, child) {
// ↑この間違いは実行するまで分からない
// アプリを起動して該当画面を開いて初めてエラーになる
return Text(profile.displayName);
})
Riverpodの解決:
// プロバイダーを定義
final lineUserProvider = StateNotifierProvider<LineUserNotifier, LineUser>((ref) {
return LineUserNotifier();
});
// 使用時
Consumer(builder: (context, ref, child) {
final user = ref.watch(lineUserProvider); // ←存在しないプロバイダーならコンパイル時にエラー
return Text(user.displayName); // ←型も自動で推論される
})
つまり:
- Provider: アプリを実行するまでエラーが分からない
- Riverpod: コードを書いた時点でエラーが分かる
2. シンプルな記述
Providerに比べて、より直感的で読みやすいコード
// まず、ライブラリをインポート
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 1. データクラスを定義(状態の形を決める)
class LineUser {
final String profileImageUrl;
final String displayName;
LineUser({required this.profileImageUrl, required this.displayName});
// copyWithで一部だけ変更できるようにする
LineUser copyWith({String? profileImageUrl, String? displayName}) {
return LineUser(
profileImageUrl: profileImageUrl ?? this.profileImageUrl,
displayName: displayName ?? this.displayName,
);
}
}
// 2. 状態を管理するNotifier
class LineUserNotifier extends StateNotifier<LineUser> {
LineUserNotifier() : super(LineUser(
profileImageUrl: 'https://example.com/default_profile.png',
displayName: 'ユーザー名',
));
void updateProfileImage(String newImageUrl) {
state = state.copyWith(profileImageUrl: newImageUrl);
// notifyListeners()は不要。stateを変更すると自動で通知される
}
}
// 3. プロバイダーの定義(これだけでアプリ全体で使える)
final lineUserProvider = StateNotifierProvider<LineUserNotifier, LineUser>((ref) {
return LineUserNotifier();
});
3. 依存関係の明確化
他のプロバイダーとの関係が分かりやすい
// 他のプロバイダーに依存するプロバイダーも簡単に作れる
final lineUserProfileImageProvider = Provider<String>((ref) {
final user = ref.watch(lineUserProvider); // ←lineUserProviderを監視
return user.profileImageUrl; // プロフィール画像のURLだけを提供
});
final lineUserDisplayNameProvider = Provider<String>((ref) {
final user = ref.watch(lineUserProvider); // ←同じプロバイダーを監視
return user.displayName; // 表示名だけを提供
});
// 例:オンライン状態は別のプロバイダーで管理
final isOnlineProvider = StateProvider<bool>((ref) => false);
// LINEユーザーの表示に必要な情報をまとめたプロバイダー
final lineUserDisplayProvider = Provider<Map<String, dynamic>>((ref) {
final user = ref.watch(lineUserProvider);
final isOnline = ref.watch(isOnlineProvider);
return {
'profileImageUrl': user.profileImageUrl,
'displayName': user.displayName,
'isOnline': isOnline,
};
});
4. テストのしやすさ
テスト用のデータを簡単に差し替えられる
// テスト用のモックデータ
class MockLineUserNotifier extends LineUserNotifier {
@override
LineUser build() {
return LineUser(
profileImageUrl: 'https://example.com/test_profile.png',
displayName: 'テストユーザー',
);
}
}
// テストコード
testWidgets('LINEプロフィールアイコンのテスト', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
// 本物のlineUserProviderをテスト用に差し替え
lineUserProvider.overrideWith(() => MockLineUserNotifier()),
],
child: MaterialApp(
home: LineProfileIcon(), // テストしたいWidget
),
),
);
// テストユーザーの名前が表示されているかチェック
expect(find.text('テストユーザー'), findsOneWidget);
});
実践的な使い方
RiverpodでLINEアプリのプロフィール画面を作ってみよう
// アプリ全体をProviderScopeでラップ(Riverpodを使うために必須)
class LineApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ProviderScope( // ←Riverpodの管理領域
child: MaterialApp(
home: LineProfilePage(),
),
);
}
}
// LINEのプロフィールアイコンWidget(Riverpod版)
class LineProfileIcon extends ConsumerWidget { // ←ConsumerWidgetを継承
@override
Widget build(BuildContext context, WidgetRef ref) { // ←WidgetRefが追加される
final user = ref.watch(lineUserProvider); // ←refでプロバイダーを監視
return CircleAvatar(
radius: 30,
backgroundImage: NetworkImage(user.profileImageUrl),
child: user.profileImageUrl.isEmpty
? Icon(Icons.person, color: Colors.grey)
: null,
);
}
}
// プロフィール編集画面
class LineProfileEditPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(lineUserProvider);
final userNotifier = ref.read(lineUserProvider.notifier); // ←変更用
return Scaffold(
appBar: AppBar(title: Text('プロフィール編集')),
body: Column(
children: [
LineProfileIcon(), // アイコン表示
Text('表示名: ${user.displayName}'),
ElevatedButton(
onPressed: () {
// プロフィール画像を変更
userNotifier.updateProfileImage('https://example.com/new_profile.png');
},
child: Text('プロフィール画像を変更'),
),
],
),
);
}
}
まとめ
少々長くなってしまいましたが、FlutterにおけるStateは?というところから状態管理のメリット、簡単な状態管理ライブラリの使い方まで網羅的に書かせていただきました。
引き続きFlutterの学習を進める上で気になった点はまとめていきます。