【Flutter/Dart】Providerで状態管理を実装する方法!MVVM設計での活用

この記事からわかること
- Flutter/DartのProviderの使い方
- 状態管理の実装方法
- ChangeNotifierやMultiProvider、Consumerとは?
index
[open]
\ アプリをリリースしました /
環境
- Android Studio:Koala
- Xcode:16.0
- Flutter:3.29.2
- Dart:3.7.2
- Provider:6.1.2
- Mac M1:Sonoma 14.6.1
Providerとは?
「Provider」はFlutterでWidget間でデータ(状態)を共有する仕組みを提供するパッケージです。FlutterにはInheritedWidget
という組み込みの状態管理機能が存在していますが、処理が煩雑で少し使いづらさがあります。このInheritedWidget
をラッパーして使いやすくしているのが「Provider」になります。
「Provider」を使用するメリットは「無駄な再描画(リビルド)を減らしパフォーマンスを向上できる」ことと「親Widgetと子Widgetでのデータの受け渡しが簡単になる」ことかなと思います。これはStatefulWidget
を使わずStatelessWidget
でWidgetを実装できる点も大きく関わっています。StatefulWidget
ではsetState
メソッドを実行した際に該当のウィジェット全体がリビルドされる特徴があります。しかしこれは状態管理が増えるほどリビルドも多くなりパフォーマンスにも影響を及ぼしかねません。
ProviderではStatelessWidget
で実装でき、かつ特定のWidgetだけを更新できる仕組みによりパフォーマンスの低下を最小限に留めることが可能になります。
※ 今の主流は「Provider」ではなく上位互換の「Riverpod」です。「Riverpod」は「Provider」と同じ開発者が開発し、「Provider」の弱点を修正する形で実装された状態管理のパッケージです。
導入方法
FlutterでProviderを利用できるようにするために以下のコマンドを実行してパッケージを導入します。
$ flutter pub add provider
これでパッケージの導入が完了し、import
文を追加すれば使用できるようになります。
import 'package:provider/provider.dart';
カウンターアプリで使い方を学ぼう
Providerの実装方法を理解するために簡単なカウンターアプリを作成してみます。その前に重要になるポイントを確認しておきます。それぞれの詳細は後述するとして全体のコードを確認してみましょう。
3つのポイント
- ChangeNotifier・・・状態管理クラス
- MultiProvider・・・状態管理クラスの供給(provide)
- Consumer・・・状態の変化を観測し再描画
それぞれのポイント(役割)ごとに色々種類がありますが、今回は上記を使用した場合のサンプルコードで見ていきます。全体のコードは「Github」にアップしているので参考にしてください。
1.ChangeNotifierで状態変化を通知できるクラスの実装
ChangeNotifier
という状態の変化を通知することができるクラスを活用し、UI更新を行わせたいタイミングを発火できる状態にしておきます。これはViewModelなどに持たせたりすることが一般的かなと思います。
ChangeNotifier
をextends
する形でクラスを定義し、UI更新を行わせたいタイミングでnotifyListeners
メソッドを実行するようにしておきます。
// ChangeNotifier => 状態管理を行えるリスナー付きのクラス
// notifyListenersメソッドで変化があったことを通知する
class CounterViewModel extends ChangeNotifier {
int _count = 0;
int get count => _count; // カウンターの値を取得
void increment() {
_count++; // カウンターを増やす
notifyListeners(); // UIを更新
}
void decrement() {
_count--; // カウンターを減らす
notifyListeners(); // UIを更新
}
}
2.MultiProviderで状態管理クラスの供給
続いてChangeNotifier
クラスを子Widgetから参照できるようにします。ここでは複数登録に対応しているMultiProvider
を使用しています。
特定の画面でのみ使用したい場合は特定のWidgetで供給する方が冗長な処理が動作しなくなります。
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Provider Example',
theme: ThemeData(primarySwatch: Colors.blue),
home: MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => CounterViewModel()),
],
child: CounterChangeNotifierView(),
),
);
}
}
3.ConsumerでUIを更新
更新された際にリアルタイムにUIの再描画処理を実行するためにConsumer
を使用します。Consumer<T>
形式でChangeNotifierを継承した型を指定することでその型にアクセスできるようになり、状態をViewに埋め込んでおくことでnotifyListeners
が実行されたタイミングでそのWidgetだけを再描画させることができます。
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../viewmodels/counter_viewmodel.dart';
class CounterChangeNotifierView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Counter with Provider')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Counter Value', style: TextStyle(fontSize: 20)),
const SizedBox(height: 10),
// Consumer を使って ViewModel の値を UI に反映
Consumer<CounterViewModel>(
builder: (context, viewModel, child) {
return Text(
'${viewModel.count}',
style: TextStyle(fontSize: 40, fontWeight: FontWeight.bold),
);
},
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// デクリメントボタン
FloatingActionButton(
onPressed: () => context.read<CounterViewModel>().decrement(),
child: const Icon(Icons.remove),
),
const SizedBox(width: 20),
// インクリメントボタン
FloatingActionButton(
onPressed: () => context.read<CounterViewModel>().increment(),
child: const Icon(Icons.add),
),
],
),
],
),
),
);
}
}
状態管理クラス:ChangeNotifier
状態自体を管理するのがChangeNotifier
クラスです。ChangeNotifier
では状態が変化したことを通知するnotifyListeners
メソッドが用意されています。ChangeNotifier
は1つのクラスとして管理できるので複数のデータ型を状態に持たせることができるようになっています。
// ChangeNotifier => 状態管理を行えるリスナー付きのクラス
// notifyListenersメソッドで変化があったことを通知する
class CounterViewModel extends ChangeNotifier {
int _count = 0;
int get count => _count; // カウンターの値を取得
void increment() {
_count++; // カウンターを増やす
notifyListeners(); // UIを更新
}
void decrement() {
_count--; // カウンターを減らす
notifyListeners(); // UIを更新
}
}
ValueNotifier
Provider
を導入しなくてもFlutterにはデフォルトでValueNotifier
なるものが用意されています。これも状態自体を管理するクラスなのですが管理できるのは単一のデータ型のみです。
final counter = ValueNotifier<int>(0);
ValueNotifier
は単一のデータの状態が変化した際に自動で更新通知が発火されます。そのためcounter.value++
を実行すると変更が通知され、ValueListenableBuilder
で変更を観測 & UI更新を行うことができるようになっています。
// ValueListenableBuilderが変更を検知し、UIに反映
ValueListenableBuilder<int>(
valueListenable: counter,
builder: (context, value, _) {
return Text(
'${counter.value}',
style: TextStyle(fontSize: 40, fontWeight: FontWeight.bold),
);
},
)
Providerとは?
続いて「Provider」について整理しておきます。「Provider」は値を提供するための仕組みを構築するためのものになります。少しややこしいですが概念として「Provider」が存在し、その中にProvider<T>
クラスやChangeNotifierProvider
などが存在します。
具体的なクラス | 概要 |
---|---|
Provider<T> | 読み取り専用のイミュータブル値 |
MultiProvider | 複数のProviderをまとめて供給 |
ChangeNotifierProvider | 状態と通知機能あり |
FutureProvider | 非同期値(Future)の提供 |
StreamProvider | リアルタイムなStreamデータの提供 |
「Provider」は状態管理クラスを渡したいWidgetをラップして使用します。ラップすることで子Widgetに状態管理クラスを供給(provide)します。この供給はBuildContext
を介して行われ、ラップされたWidget以下のツリー階層でBuildContext
から状態管理クラスを参照できるようになります。
子Widgetから参照する
子Widgetから状態管理クラスを参照する方法はいくつかあります。
Provider.of<T>
シンプルな方法でデフォルトではlisten: true
になっており、状態変化で再ビルドされる。
final viewModel = Provider.of<CounterViewModel>(context);
context.read<T>()
一度だけ値を取得したい場合など再ビルドが必要ない時に使用。
final viewModel = context.read<CounterViewModel>();
context.watch<T>()
状態を監視し、変化するとWidgetを再ビルド。
final viewModel = context.watch<CounterViewModel>();
Consumer<T>()
一部のWidgetだけを状態変化時に再ビルド。
Consumer<CounterViewModel>(
builder: (context, viewModel, child) {
return Text(
'${viewModel.count}',
style: TextStyle(fontSize: 40, fontWeight: FontWeight.bold),
);
},
),
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。