※こちらの記事は【Flutter】色んな状態管理手法でカウンターアプリを作ってみるの一部として作成された記事です
今最もホットな状態管理手法 StateNotifier
さて今回は今最もホットな状態管理手法とも言えるStateNotifierでカウンターアプリを書いていきたいと思います。
使用するPackage:
- flutter_state_notifier v0.7.1
概要
- Provider、Freezed、Riverpodなどの作者でもあるRemi Rousselet氏が開発
- 単一の値を保持し、その値の変更を通知する
ValueNotiifer
の拡張版のようなクラス -
Riverpod
とFreezed
と併せて使う例がよく見られる - 初版リリースは2020年3月
全体像
特徴:
単一の状態変数を保持し、その変数の変更を自動で通知する
ChageNotifier
が複数の変数を管理し、notifyListeners
メソッドで明示的に変更を通知し、再描画などを促すのに対し、StateNotifier
は単一の状態変数だけをスコープに持ち、その変数に変化があった場合、自動的に通知を飛ばす。
その為、複数の状態変数を管理したい場合は、状態クラスに複数のフィールドを持たせ管理する事になります。
実際には状態クラスが持つフィールドを直接変更する事も出来ますが、状態クラスはデータを保持するだけのクラスなのでなるべくimmutableな管理をする事でより堅牢な管理が可能になります。
immutableな管理を行う事が多い為、freezed
などimmutableなクラスを生成するツールと組み合わせる事が多いようです。
依存関係の注入:
依存関係の注入は同じ開発者が作成したProvider
、Riverpod
どちらかのパッケージと組み合わせて行います。
今回はStateNotifier
パッケージに用意されているStateNotifierProvider
を使いますが、provider
パッケージのProvider
と同じです。Widgetツリーに沿って依存関係を下に流していきます。
キーとなるクラスやメソッド
-
StateNotifier
クラス:状態管理クラスが継承するクラス -
StateNotifierProvider
クラス:状態管理クラスを注入するクラス
準備
具体的にカウンターアプリを例に見ていきましょう。
サンプルコードはこちら
1. Stateクラス
前述の通り、immutableな状態クラスを定義します。
@immutable
class CounterState {
CounterState({this.count});
final int count;
}
freezed
などと組み合わせる事が多い様ですが、今回はシンプルにする為、そういったパッケージは用いません。
2. 状態管理クラス
-
StateNotifier
クラスを継承する状態管理クラスを定義します。 -
StateNotifier
が管理する状態クラスの型を明示します。(今回はCounterState
クラス) -
state
で管理している状態変数にアクセスする事ができます。 -
コンストラクタで渡す初期値も、状態値を変更するメソッドでも
state
に新しいCounterState
インスタンスを代入しています。
class CounterStateNotifier extends StateNotifier<CounterState> {
CounterStateNotifier() : super(CounterState(count: 0));
void increment() => state = CounterState(count: state.count + 1);
void decrement() => state = CounterState(count: state.count - 1);
void reset() => state = CounterState(count: 0);
}
3. 依存関係の注入
-
StateNotifier
パッケージに用意されているStateNotifierProvider
クラスを使って、依存関係を注入します。 - とはいえ裏で
provider
パッケージを使っているので実際には同じprovider
です。 -
-
create
フィールドで状態管理クラスCounterStateNotifier
をインスタンス化
-
-
child
フィールドに定義した_StateNotifierCounterPage
widgetにインスタンスを注入
@override
Widget build(BuildContext context) {
return StateNotifierProvider<CounterStateNotifier, CounterState>(
create: (context) => CounterStateNotifier(),
child: const _StateNotifierCounterPage(),
);
}
これで準備は整いました。
状態へのアクセス
前述の通り、裏でprovider
を使ってるので状態へのアクセスもprovider
と同じcontext.watch
とcontext.read
を使いたいと思います。
状態値の監視
-
context.watch()
で状態値の変更を監視し、変更に応じてwidgetを再描画します。 - ただ
build
メソッド内でcontext.watch()
を定義してしまうと状態が変化する度にwidgetが丸ごと再描画されてしまいます。
Widget build(BuildContext context) {
// ProviderCounterStateの値が変わる度にbuildメソッドが走る
final ProviderCounterState state =
context.watch<CounterStateNotifier>();
return Scaffold(
appBar: MainAppBar(
title: 'StateNotifier x Provider',
...
- これでは再描画する単位が大きくパフォーマンスが悪いので、変更に応じてラップしているwidgetだけを再描画する
Consumer
クラスを使用する事で再描画される範囲を限定する事が出来ます。
Consumer<CounterState>(
builder: (context, state, _) => Text(
state.count.toString(),
style: Theme.of(context).textTheme.headline4,
),
),
- 実際に状態値を参照して描画しているText widgetだけを
Consumer
クラスでラップする事で状態値が変更しても再描画されるのはラップされたText Widgetだけになりました。
メソッドへのアクセス
状態変数を変更するメソッドへのアクセスは単発的に状態管理クラスにアクセスするcontext.read()
を使います。
FloatingActionButton(
onPressed: () => context.read<CounterStateNotifier>().increment(),
tooltip: 'Increment',
heroTag: 'Increment',
child: Icon(Icons.add),
),
全体
import 'package:flutter/material.dart';
import 'package:flutter_state_notifier/flutter_state_notifier.dart';
import 'package:provider/provider.dart';
import 'package:state_management_examples/widgets/main_appbar.dart';
import 'package:state_notifier/state_notifier.dart';
// 状態クラス
@immutable
class CounterState {
CounterState({this.count});
final int count;
}
// 状態管理クラス
class CounterStateNotifier extends StateNotifier<CounterState> {
CounterStateNotifier() : super(CounterState(count: 0));
// when not using freezed, you need to substitute new State into managed state
void increment() => state = CounterState(count: state.count + 1);
void decrement() => state = CounterState(count: state.count - 1);
void reset() => state = CounterState(count: 0);
}
// 依存関係の注入
class StateNotifierCounterPage extends StatelessWidget {
const StateNotifierCounterPage({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return StateNotifierProvider<CounterStateNotifier, CounterState>(
create: (context) => CounterStateNotifier(),
child: const _StateNotifierCounterPage(),
);
}
}
// カウンター本体
class _StateNotifierCounterPage extends StatelessWidget {
const _StateNotifierCounterPage({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
print('rebuild!');
return Scaffold(
appBar: MainAppBar(
title: 'StateNotifier x Provider',
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
// Consumer is also valid for stateNotifier solution to narrow the rebuild scope
Consumer<CounterState>(
builder: (context, state, _) => Text(
state.count.toString(),
style: Theme.of(context).textTheme.headline4,
),
),
],
),
),
floatingActionButton: FittedBox(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
FloatingActionButton(
onPressed: () => context.read<CounterStateNotifier>().increment(),
tooltip: 'Increment',
heroTag: 'Increment',
child: Icon(Icons.add),
),
const SizedBox(width: 16),
FloatingActionButton(
onPressed: () => context.read<CounterStateNotifier>().decrement(),
tooltip: 'Decrement',
heroTag: 'Decrement',
child: Icon(Icons.remove),
),
const SizedBox(width: 16),
FloatingActionButton.extended(
onPressed: () => context.read<CounterStateNotifier>().reset(),
tooltip: 'Reset',
heroTag: 'Reset',
label: Text('RESET'),
),
],
),
),
);
}
}
以上でした
いかがでしたでしょうか?
State Notifier
を解説する記事はStateNotifier x Riverpod (x Freezed)
という構成で書かれているが非常に多いですが、今回は「状態管理パッケージと依存注入パッケージ」は違うんだという事を分かってもらいたくて、あえてRiverpod
を使わないで紹介してみました。
とはいえ強い人に状態管理と依存注入で併せて状態管理だろ、と言われたら「へへー(平伏)」となる程度に個人的な理解なので、実際に触ってみて各々のわかりやすい理解をしていって頂いたら良いかなと思います