Flutterの状態管理(StatefulWidget,provider,riverpod)についてそれぞれ簡単に説明・比較してみます。
そもそも状態管理とは?
20年前のホームページとは違い、最近のアプリやWebは、ユーザの操作によって動的に画面が変わります。
例えば、スマホの設定画面でWiFiのスイッチを押すと、スイッチが動的にその場で切り替わります。
例えば、Twitterでダークモードを選択するとその場で画面が真っ黒になります。
このように使われるアプリケーションの状態(State)を反映するように画面を描画することを宣言的(declarative)と言います。Flutterはこの宣言的UIが作れるようにできています。
宣言的UIを実現するためにはアプリケーションの状態(State)を管理(management)する必要があります。
参考: https://flutter.dev/docs/development/data-and-backend/state-mgmt/intro
ん、具体的に何を管理するの?
例えば・・・
認証したユーザの名前やアイコン(ログインしてるのか否かも含む)
たくさんあるリストのチェックボックスにチェックが入ったかどうか
WiFiをONにするかOFFにするか
書き込んだ日付
っていうか画面を描画するために必要な変数全般です。逆に、描画に関係ない情報は当たり前ですが普通持ちません。
状態管理のサンプル
有名そうなパターンについて簡単なサンプルコードを作ってみたので、それらを解説しながら具体的に説明していきます。
それぞれの方法の詳細やアーキテクチャはそれぞれの公式を参照してください。
カウンターアプリをいろんな方法で実装してみる
Flutterのプロジェクトを立ち上げた時にでき上がるアプリ(ボタンを押したことをカウントする)を敢えてそれぞれの状態管理を使って実現してみます。
ボタンを押した回数を今回状態として保持します。
(プロジェクト作成時に出来上がるlib/main.dart
の中身とpubspec.yml
を書き換えて実行してください。)
StatefulWidget
簡単に説明
Flutterのあらゆる教材で一番最初に出てきます。まっさらなflutterアプリを作るとこのコードが最初から入っているはずです。
setState
という関数をウィジェットが入っているクラスの中で使うことによって、状態の変更を更新することができます。
1個2個の状態であればこれで十分ですが、たくさんの状態を管理することになると、setStateばっかりになってしまい、どこでどんなふうな変更したか追いかけるのが大変になってしまいます。
コード(lib/main.dart)
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
// 2.呼び出されると状態が更新されて画面に反映される
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter, // 1.ボタンを押したら関数が呼ばれる
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
Provider
簡単に説明
Flutter公式でもおすすめされている状態管理の方法です。
ChangeNotifierを継承したクラスで状態を定義します。notifyListnersを都度書いて画面更新することになります。
オブジェクトをリッスンしないと描画できません。
コード(lib/main.dart)
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// 1.状態を定義する(ChangeNotifierを拡張したクラスを定義する)
class CountState extends ChangeNotifier {
int count = 0;
void addCount() {
count++;
// これがないと描画してくれない
notifyListeners();
}
}
void main() {
runApp(CountApp());
}
class CountApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// CounterStateを呼び出す、ただしWidgetの中で!
final CountState countState = CountState();
// 2.ウィジェット全体をChangeNotifierProviderで囲む
return ChangeNotifierProvider<CountState>.value(
value: countState,
child: MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('CounterApp')),
body: _Body(),
floatingActionButton: _FloatingActionButton(),
),
),
);
}
}
class _Body extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 3.Stateを取り出す
final CountState countState = Provider.of<CountState>(context);
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
countState.count.toString(),
style: Theme.of(context).textTheme.headline4,
),
],
),
);
}
}
class _FloatingActionButton extends StatelessWidget {
Widget build(BuildContext context) {
// 3.Stateを取り出す
final CountState counterState = Provider.of<CountState>(context);
return FloatingActionButton(
onPressed: () => counterState.addCount(),
child: Icon(Icons.add),
);
}
}
Riverpod
簡単に説明
providerの作者の方がより使いやすくなるように作り直した状態管理のパッケージがriverpodです。
グローバルに定義することができるので、どこのウィジェットからでも状態を読み書きできます。
また、実行時ではなくコンパイル時にプログラミングエラーを見つけることができます。
以下の流れで使うことができます。
①まず最初にApp全体をProviderScopeで囲む
②StateProviderを使ってProviderを宣言。グローバルに宣言
③Listenしておかなくても、providerを読んで更新できる
(④ConsumerWidgetを使って、providerを読む)
コード(lib/main.dart)
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(
// ①まず最初にApp全体をProviderScopeで囲む
const ProviderScope(child: MyApp()),
);
}
class MyApp extends StatelessWidget {
const MyApp({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(home: Home());
}
}
/// ②StateProviderを使ってProviderを宣言。グローバルに宣言。
final StateProvider counterProvider = StateProvider((ref) {
return 0;
});
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Counter')),
body: Center(
child: GoodText(context),
),
floatingActionButton: FloatingActionButton(
// ③Listenしておかなくても、providerを読んで更新できる。
onPressed: () => context.read(counterProvider).state++,
child: const Icon(Icons.add),
),
);
}
}
class GoodText extends ConsumerWidget {
GoodText(BuildContext context);
Widget build(BuildContext context, ScopedReader watch) {
// ④ConsumerWidgetを使って、providerを読む
final count = watch(counterProvider).state;
print("iine is rewrite $count");
return Text('$countいいね');
}
}
Riverpod + Flutter Hooks
簡単に説明
React Hooksっぽく書くことができます。
コード(lib/main.dart)
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:state_notifier/state_notifier.dart';
void main() {
// 1.まず最初にApp全体をProviderScopeで囲む
runApp(
ProviderScope(
child: CounterApp(),
),
);
}
// 2.StateNotifierProviderを使ってcounterProviderを宣言。グローバルに宣言。
final counterProvider = StateNotifierProvider((_) => Counter());
// StateNotifier<**> -> **はStateという変数の型になる
class Counter extends StateNotifier<int> {
Counter() : super(0);
void increment() => state++;
}
// Note: CounterApp is a HookWidget, from flutter_hooks.
class CounterApp extends HookWidget {
@override
Widget build(BuildContext context) {
// 3.useProviderを使って状態を取得
final state = useProvider(counterProvider.state);
final counter = useProvider(counterProvider);
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('CounterApp')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
state.toString(),
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
//作って置いた関数を呼び出して値を変えることもできる
onPressed: () => counter.increment(),
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
),
);
}
}
で、どれ使えば良い?
上記のコードだけで判断しないでください・・・。一通り公式を見ながらサンプル実装してみた方がいいです。
グローバルに定義できてどこからでも状態を読み書きできるriverpodに慣れてしまうと便利で辞められないというのが個人的な意見です。
一方で宗教上の都合で(どんな宗教か分かりませんが)Flutter公式しか使いたくないならproviderがよいと思います。
また、簡単な画面1枚の趣味アプリだったらStatefulWidgetでも十分かもしれません。
既に開発したプロダクトの規模がある程度大きくなっていて、既に使っている状態管理があればそれを使った方が良いです。
また、複数人で作る場合はどの状態管理を使うか明示的に共有しないとカオスになるので、そこはチームで話し合ってどれにするか決めた方がいいはずです。
Flutterは新しいパッケージが次々と出てくるのであと数ヶ月したらもうdeprecatedなんてこともあるかもしれません。
その時期その時期の良い方法を使っていった方が良いと思いますS。