現在Flutter界隈で見られる状態管理は
- Statefull Widget
- ChangeNotifier + Provider
- StateNotifier + Provider ( + freezed)
- StateNotifier + Riverpod
下に行くほど最新のものになっています。
BLoCパターンやReduxは分からないので扱いません。公式でもProviderが推奨されているみたいです。
この記事では、カウントアプリを4つの状態管理方法で作成し、比較してみようと思います。
見た目はこんな感じ。デフォルトで表示される物をいじっていきます。
Statefull Widget
簡単に、Statefull Widgetが状態を保つ仕組みを説明します。
StatelessWidget, StatefullWidget クラスはbuildのたびに作られますが、Stateクラスは一度作られたらそのまま保持されます。StatefullWidgetは紐付けたStateを参照して一貫性を保ちます。
setStateで状態を変更すると、UIに変更が通知され、該当の部分だけ再描写されるようになっています。
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text(
_counter.toString(),
style: Theme.of(context).textTheme.headline4,
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
StatelessWidgetは不変なので変更があるたびWidgetを表示し直す、StatefullWidgetは変更部分だけがうまく再描写されるようになっているので効率的です。
しかし、アプリケーション全体の状態を管理するには向いてません。ロジックがUIの中で書かれてしまい、UIとの分離ができないからです。UIの状態のみ(タップされた、スクロールしている、など)を管理する場合には良いでしょう。
ChangeNotifier + Provider
ChangeNotifierは状態を保持し、変更を通知するクラスです。
Widgetクラスの中で、Providerを作ります。その中でChangeNotifierをインスタンス化します。Providerで囲われている範囲でUIはChangeNotifierインスタンスにアクセスすることができます。StatelessWidgetの中で使えます。
import 'package:provider/provider.dart';
...
// このクラスが状態管理、変更を担う
class CounterNotifer extends ChangeNotifier {
var counter = 0;
void incrementCounter() {
counter++;
notifyListeners();
}
}
// Stateless WidgetでOK
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => CounterNotifer(),
child: CounterPage(),
);
}
}
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Consumer<CounterNotifer>(
// 囲う範囲は最小限にする。
// Consumerで囲まず、context.watch<CounterNotifier>().counter でも取得可
builder: (context, countNotifier, child) => Text(
countNotifier.counter.toString(),
style: Theme.of(context).textTheme.headline4,
),
),
),
floatingActionButton: FloatingActionButton(
// 状態を変更するだけで、監視する必要がないので listen: false
onPressed: Provider.of<CounterNotifer>(context, listen: false)
.incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
状態管理したい場所をProviderで囲うのでどうしても階層が深くなってしまいます。Providerを使う時に、contextを渡さないといけないので、build関数の中でしか使えません。
アプリケーション全体で使うような状態が複数ある場合、一括で複数のProviderを登録する、MultiProvidersで一番ルートに近い場所に置いておくのが分かりやすいです。
Stateless Widgetで非同期処理をする場合、FutureBuilderを使いbuild関数の中で実行する方法がありますが、UIの変更があるたびに、毎回リビルド(再描写)されてしまい、あまりスマートではありません。
非同期処理ならStatefull WidgetのBuildContextを呼べる関数(例えば didChangeDependencies ) の中で使った方がまだ良いです。StatefulWidgetも毎回リビルドしますが、効率的にされます。
このように、ProviderはBuildContextがある状況でしか呼べないのが辛いところです。その辛みを解消したRiverpodが注目を集めています。また、Widgetツリーに関係なく状態を管理したい時は、GetItも選択肢に入るかもしれません。
参考:https://qiita.com/gki/items/eedcdcc9eb446be989c6
StateNotifier + Provider
ChangeNotifierの変数の変更を通知するのには、関数内で変更をするたびに notifylisteners を呼ぶ必要がありました。この煩わしさを解決したのがStateNotifierです。名前から汲み取ると、ChangeNotifier(変更うを通知) からStateNotifier(状態そのものを通知) となり、より宣言的に状態管理ができていると思いました。
*後で知ったのですが、StateNotifierは元からある ValueNotifierをChangeNotifier並みの使い方ができるように拡張したものだそうです。
import 'package:state_notifier/state_notifier.dart';
import 'package:flutter_state_notifier/flutter_state_notifier.dart';
...
class CounterNotifer extends StateNotifier<int> {
CounterNotifer() : super(0);
void incrementCounter() {
state++;
}
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StateNotifierProvider<CounterNotifer, int>(
create: (_) => CounterNotifer(),
child: CounterPage(),
);
}
}
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text(
context.watch<int>().toString(),
style: Theme.of(context).textTheme.headline4,
),
),
floatingActionButton: FloatingActionButton(
onPressed: Provider.of<CounterNotifer>(context, listen: false)
.incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
StateNotifierではstate を一つだけ持ちます。stateが変更されると、listener全員に通知されます。Widget側の、Provider.of
、またcontext.watch
がリスナーということになり、変更を監視できます。
気をつけるポイントは
-
StateNotifierProvider<CounterNotifer, int>
とStateNotifier とstateの型どちらも与えること -
context.watch<CounterNotifier>()
ではなくcontext.watch<int>()
でstateを取得する、ということです。
Note that watching MyController will not cause the listener to rebuild when StateNotifier.state updates.
https://pub.dev/documentation/flutter_state_notifier/latest/flutter_state_notifier/StateNotifierProvider-class.html
StateNotifier + Provider + freezed
一つのStateNotifier
につき一つのstate
しか作れないので、複数の状態を管理したい時はまとめてオブジェクトを作って管理します。
この例では、freezedパッケージを使い、不変ImmutableなStateクラスを作成します。stateを変更したい時は新しく状態を作成して、代入します。これにより、stateが変更されたかどうかを監視しやすくしています。そして変更を検知して、UIに自動で変更を通知してくれます。
freezedパッケージは少々使い方が面倒です。ジェネレーターというものを使い、コードを生成します。生成されたコードではcopyWith
という関数が作られ、一部を変更して代入するのが簡単にできるようになります。 詳しくは https://pub.dev/packages/freezed
freezedパッケージを導入し CounterState
クラスを作ります。
dependencies:
freezed_annotation:
dev_dependencies:
analyzer: 0.39.14
build_runner:
freezed:
*flutter 1.20ではanalyzerを指定しなきゃエラーになりました。https://github.com/rrousselGit/freezed/issues/245
// counter_state.dart
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'counter_state.freezed.dart';
@freezed
abstract class CounterState with _$CounterState {
factory CounterState({int count, bool isActive}) = _CounterState;
}
ターミナルでジェネレーターを起動します。
flutter pub run build_runner build
これで、copyWith
が使えるようになりました。
// CounterNotifier
class CounterNotifer extends StateNotifier<CounterState> {
CounterNotifer() : super(CounterState(count: 0, isActive: true));
void incrementCounter() {
if (state.isActive) state = state.copyWith(count: state.count + 1);
}
}
Widgetで使う時は
context.watch<CounterState>().count.toString(),
のように使います。他にもcontext.read, select
などが使えます。
Riverpod
これはProviderと同じ作者が新しく実験的に作っている新しくProviderを書き直したものです。flutterに依存しないピュアなDartパッケージとして作っているそう。
This project can be considered as an experimental provider rewrite
https://pub.dev/packages/riverpod
ProviderをBuildContextに依存せず、どこでも宣言できます(すごい)
さっきのConterNotifierを使って、書き直してみます。
final counterProvider = StateNotifierProvider((ref) => CounterNotifier());
void main() {
runApp(
ProvierScope(
MyApp()));
}
スコープの範囲で使用できます。ProviderScope
はルートに書くことを推奨しています。
使う時は、Consumerで囲います。
Center(
child: Consumer((context, watch) {
final count = watch(counterProvider).count;
return Text('$count');
}),
)
Flutter Hooksを使うと useProvider(hogeProvider).state
などと使うことができます。
公式の使い方が分かりやすくて詳しいので、ぜひ読んで見てください。
https://riverpod.dev/
まだ実験段階でライブラリが安定していないので、使う時には注意が必要です。
この記事はクロス投稿です。
https://shuent.qrunch.io/entries/MBooHnzeTUj2bnZp