こんにちは、ゲームソリューション部のsoraです。
今回は、Flutterの状態管理パッケージ Provider・Riverpod(flutter_riverpod
)を使ってそれぞれのコードを見比べてみたことについて書いていきます。
今回実装したアプリ
今回実装したアプリは以下です。
プロジェクト作成時のコードを少し変えたものです。
このアプリを状態管理パッケージごとにそれぞれ実装して、見比べていきます。
なぜ状態管理パッケージを使うのか
まずは、なぜ状態管理パッケージを使うのかという話からします。
状態管理パッケージを使わない場合、状態が変化するものに対しては、StatefulWidgetを継承したWidgetを作成して、その中でStateクラスを使用します。
この場合、1つのStateを複数の他のWidgetで参照する場合に、親ウィジェットを経由して状態を渡す必要があるのですが、これだと状態を使用したいWidgetにたどり着くまでの部分の全てのWidgetに更新が入ってしまい処理効率が悪いです。
またコードも複雑化します。
Widgetツリーの話なのですが、以下記事がわかりやすかったです。
[Flutter] Provider - 今回のブログポストではFlutterでグローバル状態(State)、またはウィジェットたちの間で状態(State)を共有するためProviderを使う方法について説明します。
これが状態管理パッケージを使用すると、各Widgetへ直接状態の受け渡しをすることができ、無駄な箇所の更新を入れずに効率よく再描画することができるようになります。
そのため、Flutterでは状態管理パッケージを使用することが一般的になっているとのことです。
主な状態管理パッケージ
主な状態管理パッケージは以下です。
詳細な解説については、私もまだ使い始めたばかりのため、今回は割愛します。
・Provider
公式も推奨している状態管理パッケージ
・Riverpod
機能的にProviderの上位互換である状態管理パッケージ
Riverpodの中でも、3パターン存在する
- Flutterで使用する場合、flutter_riverpod
- Dart単体で使用する場合、riverpod
- Flutter Hooksと併用する場合、hooks_riverpod
今回は、flutter_riverpod
を使用します。
状態管理パッケージなし
まず、状態管理パッケージなしのコードは以下です。
プロジェクト作成時のコードを少し変えただけなので、説明は割愛します。
main.dart
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('Provider test'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: _incrementCounter,
child: const Text('Count Up'),
),
]
)
],
),
),
);
}
}
状態管理パッケージ Providerを使用
次にProviderを使用して実装します。
以下ページをもとにパッケージをインストールします。
provider | Flutter package
flutter pub add provider
Providerを使用して作成したコードは以下です。
細かい部分の説明はコード内のコメントに記載していますが、StatefulWidget+Stateの形ではなく、StatelessWidgetで記述できていることがわかります。
main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'model.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(),
home: const MyHomePage(),
);
}
}
// ここまで状態管理パッケージなしのパターンと同じ
// Providerを使うことで、StatefulWidget+Stateの形ではなく、StatelessWidgetになっている
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});
@override
Widget build(BuildContext context) {
// 状態の変更を検知するChangeNotifierProvider
return ChangeNotifierProvider<CounterModel>(
// model.dartで定義したモデルを作成
create: (_) => CounterModel(),
child: Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('Provider test'),
),
// 状態の変更を受け取って再描画する範囲をConsumerで囲む
body: Consumer<CounterModel>(builder: (context, model, child){
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
// model.dartで定義したクラスのインスタンスから値を取得
model.counter.toString(),
style: Theme.of(context).textTheme.headlineMedium,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
child: const Text('Count Up'),
onPressed: (){
// model.dartで定義したクラスのインスタンスのメソッドを実行
model.incrementCounter();
},
),
],
),
],
),
);
})
),
);
}
}
main.dart
とは別で、モデルを定義するmodel.dart
を作成しています。
model.dart
import 'package:flutter/material.dart';
// クラスの変更を監視できるProviderであるChangeNotifierを継承したクラスを定義
class CounterModel extends ChangeNotifier {
int _counter = 0;
int get counter => _counter;
void incrementCounter() {
_counter++;
// notifyListeners()で値の変更をリスナーへ通知
notifyListeners();
}
}
状態管理パッケージ Riverpod(flutter_riverpod)を使用
最後にRiverpod(flutter_riverpod
)を使用して実装します。
以下ページをもとにパッケージをインストールします。
flutter_riverpod | Flutter package
flutter pub add flutter_riverpod
Riverpod(flutter_riverpod
)を使用して作成したコードは以下です。
細かい部分の説明はコード内のコメントに記載していますが、こちらもProviderパッケージを使用したパターンと同様、StatefulWidget+Stateの形になっていないことがわかります。
flutter_riverpodには、ProviderやNotifierProviderなどの様々な種類のProviderが存在します。
詳細について知りたい方は、以下の記事が各種類に対してコード付きで説明されていて、わかりやすかったのでおすすめです。
Riverpod 2.x の Provider まとめ
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Notifierクラスを定義
class Counter extends Notifier<int> {
final count = 0;
@override
int build() {
return count;
}
void increase() {
state++;
}
}
// NotifierProviderをグローバルに定義
final countProvider = NotifierProvider<Counter, int>(() {
return Counter();
});
void main() {
runApp(
// Providerを全体で利用可能にするためにProviderScopeでスコープを指定
const ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(),
home: const MyHomePage(),
);
}
}
class MyHomePage extends ConsumerWidget {
const MyHomePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('Provider test'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
// ref.watch(StateProvider)で対象のProviderの状態取得と変更検知
// ref.read(StateProvider)で対象のProviderの状態取得のみ(変更検知は行わない)
"${ref.watch(countProvider)}",
style: Theme.of(context).textTheme.headlineMedium,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
child: const Text('Count Up'),
onPressed: (){
// Notifier内でメソッドを使用してカウントを増加させる
ref.watch(countProvider.notifier).increase();
},
),
]
),
],
),
),
);
}
}
最後に
今回は、Flutterの状態管理パッケージ Provider・Riverpod(flutter_riverpod
)を使ってそれぞれのコードを見比べてみたことを記事にしました。
個人的にRiverpodが一番書きやすくて読みやすいと思ったので、今後は一旦Riverpod(flutter_riverpod
)を使っていこうかなと思います。
後々はFlutter Hooksと合わせて、Riverpod(hooks_riverpod
)も使ってみようと思います。
本記事がどなたかの参考になると幸いです。