元記事
https://docs.flutter.dev/get-started/fundamentals/state-management
意訳しています.
始めに
Flutterの状態はUIや状態管理システムを表示するために使うすべてのオブジェクトを参照する.
状態管理とは,最も効率的にオブジェクト同士でアクセスするために私たちのアプリを整理し,ウィジェット間でオブジェクトを共有する方法である.
このページでは状態管理の多くの側面を見ていく.
-
StatefulWidget
を使うこと - コンストラクタ,
InheritedWidget
,コールバックを使ってウィジェット間で状態を共有すること - 何かが変更されたとき,他のウィジェットに知らせるために
Listenable
を使うこと - アプリケーションアーキテクチャは
Model-View-ViewModel
アーキテクチャになっていること
以下のビデオを見てもよいだろう.
https://youtube.com/watch?v=vU9xDLdEZtU
これはhttps://pub.dev/packages/riverpodの使い方を紹介している.
このページではサードパーティ製のパッケージは使わないが,それを使っても問題ない.
StatefulWidgetを使う
状態を行う最も単純な方法はStatefulWidget
を使うことである.
StatefulWidget
は状態を保存する.
例えば,以下のウィジェットについて考えてみる.
class MyCounter extends StatefulWidget {
const MyCounter({super.key});
@override
State<MyCounter> createState() => _MyCounterState();
}
class _MyCounterState extends State<MyCounter> {
int count = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Count: $count'),
TextButton(
onPressed: () {
setState(() {
count++;
});
},
child: Text('Increment'),
)
],
);
}
}
状態管理について考えるとき,このコードには2つの重要な要素がある.
- カプセル化 :
MyCounter
を所有するウィジェットはcount
変数にアクセスすることはできず,変更もできない - オブジェクトライフサイクル :
MyCounter
が作られたときに,_MyCounterState
オブジェクトとcount
変数が作られる.それはスクリーンから取り除かれない限り存続する.これは一時的な状態の例である
詳細は以下のサイトを確認すること.
https://docs.flutter.dev/data-and-backend/state-mgmt/ephemeral-vs-app
https://api.flutter.dev/flutter/widgets/StatefulWidget-class.html?_gl=1*3d90dz*_ga*MTY0NTg1NDE5MC4xNzQwMzc3NjA0*_ga_04YGWK0175*MTc0MDU1NzYyNy41LjEuMTc0MDU2NDM5OC4wLjAuMA..
ウィジェット間で状態を共有する
以下の例のように,いくつかのシナリオではアプリは状態を保存する必要がある.
- 状態を更新し,アプリの他のウィジェットに通知する
- 共有された状態が更新されることを受け取り,状態が更新されたときにUIを再構築する
このセクションでは異なるウィジェット間で効率的に状態を共有する方法を示す.
最も一般的なパターンは,
- ウィジェットコンストラクタを使う
-
InheritedWidget
を使う - 何かが変更されたことを親ウィジェットに知らせるコールバック
ウィジェットコンストラクタを使う
Dartオブジェクトは参照によって渡されるため,コンストラクタの中でウィジェットを使うためにオブジェクトを定義することはウィジェットにとって一般的である.
コンストラクタに渡されたどんなオブジェクトもUIを構築するために使われる.
class MyCounter extends StatelessWidget {
final int count;
const MyCounter({super.key, required this.count});
@override
Widget build(BuildContext context) {
return Text('$count');
}
}
これにより,ウィジェットの他のユーザが,それを使用するために何を提供する必要があるかを知ることができる.
Column(
children: [
MyCounter(
count: count,
),
MyCounter(
count: count,
),
TextButton(
child: Text('Increment'),
onPressed: () {
setState(() {
count++;
});
},
)
],
)
ウィジェットコンストラクタを通じて共有されたデータにアクセスすることは,共有された依存関係をコードの中で読むことができ,大変分かりやすい.
この一般的な方法は依存性の注入といい,多くのフレームワークで使用されている.
InheritedWidget
を使う
手動でウィジェットツリーにデータを渡すと,冗長になり,不要な定型コードを引き起こす可能性がある.
FlutterはInheritedWidget
を提供する.
InheritedWidget
は親ウィジェットにデータを保存するため,子ウィジェットにデータを保存することなくデータにアクセスすることができる.
InheritedWidget
を使うために,InheritedWidget
クラスを拡張し,dependOnInheritedWidgetOfExactType
を使って静的メソッドof()
を実装する.
buildメソッドの中でof()
を呼ぶウィジェットはFlutterフレームワークに管理される依存性を作ることができる.
そのため,ウィジェットが新しいデータを使って再構築されたり,updateShouldNotify
がtrueになったりしたときに,InheritedWidget
に依存するすべてのウィジェットは再構築される.
class MyState extends InheritedWidget {
const MyState({
super.key,
required this.data,
required super.child,
});
final String data;
static MyState of(BuildContext context) {
// This method looks for the nearest `MyState` widget ancestor.
final result = context.dependOnInheritedWidgetOfExactType<MyState>();
assert(result != null, 'No MyState found in context');
return result!;
}
@override
// This method should return true if the old widget's data is different
// from this widget's data. If true, any widgets that depend on this widget
// by calling `of()` will be re-built.
bool updateShouldNotify(MyState oldWidget) => data != oldWidget.data;
}
次に,共有された状態にアクセスするために,ウィジェットのbuild()
メソッドからof()
メソッドを呼ぶ.
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
var data = MyState.of(context).data;
return Scaffold(
body: Center(
child: Text(data),
),
);
}
}
コールバックを使う
コールバックを公開することによって値が変化したときに,他のウィジェットに知らせることができる.
FlutterはValueChanged
型を提供する.
ValueChanged
型は一つのパラメータを使って関数コールバックを宣言する.
typedef ValueChanged<T> = void Function(T value);
ウィジェットコンストラクタの中でonChanged
を公開することによって,ウィジェットがonChanged
を呼んだときに,このウィジェットを使用しているウィジェットが応答する方法を提供する.
class MyCounter extends StatefulWidget {
const MyCounter({super.key, required this.onChanged});
final ValueChanged<int> onChanged;
@override
State<MyCounter> createState() => _MyCounterState();
}
例えば,このウィジェットはonPressed
コールバックを使って,状態が保持するcount
変数の値を更新する.
TextButton(
onPressed: () {
widget.onChanged(count++);
},
),
もっと深く見る
ここをチェック.
https://docs.flutter.dev/get-started/fundamentals/state-management#dive-deeper
Listenable
を使う
状態を共有する方法は学んだが,状態が変化したときにどのようにUIを更新するかは学んでいない.
アプリの他の部分に通知するような方法で,共有状態を変更するにはどうすればいいのだろうか?
FlutterはListenable
と呼ばれる抽象クラスを提供する.
Listenable
を使ういくつかの方法は,
-
ChangeNotifier
を使って,ListenableBuilder
を使って定期監視する -
ValueNotifier
を使って,ValueListenableBuilder
を使う
ChangeNotifier
ChangeNotifier
を使うために,ChangeNotifier
を拡張し,リスナーに知らせる必要があるときのみnotifyLister
を呼びだす.
class CounterNotifier extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
そして,それをListenableBuilder
に渡して,ChangeNotifier
がリスナーを更新するたびに,ビルダー関数が返すサブツリーが再構築されるようにする.
Column(
children: [
ListenableBuilder(
listenable: counterNotifier,
builder: (context, child) {
return Text('counter: ${counterNotifier.count}');
},
),
TextButton(
child: Text('Increment'),
onPressed: () {
counterNotifier.increment();
},
),
],
)
ValueNotifier
ValueNotifier
は単一の変数を保存するため,ChangeNotifier
の単純化したものである.
ValueListenable
とListenable
を実装するため,ListenableBuilder
とValueListenableBuilder
のようなウィジェットと互換性がある.
ValueNotifier
を使うために,初期値を使ってValueNotifier
のインスタンスを作成する.
ValueNotifier<int> counterNotifier = ValueNotifier(0);
変数を見たり更新したりするためにvalue
フィールドを使い,変数が更新されたことをListenerを通じて知らせる.
ValueNotifier
はChangeNotifier
を拡張したものであるから,Listenable
であり,ListenableBuilder
を使える.
builder
コールバックの中で値が提供されるValueListenableBuilder
も使える.
Column(
children: [
ValueListenableBuilder(
valueListenable: counterNotifier,
builder: (context, value, child) {
return Text('counter: $value');
},
),
TextButton(
child: Text('Increment'),
onPressed: () {
counterNotifier.value++;
},
),
],
)
もっと深く見る
ここをチェック.
https://docs.flutter.dev/get-started/fundamentals/state-management#deep-dive
アプリケーションアーキテクチャであるMVVMを使う
状態を共有し,状態が変更されたときにアプリの他の部分に通知する方法を理解したので,アプリ内のステートフルオブジェクトをどのように整理するかを考え始める準備ができた.
このセクションは,Flutterのようなフレームワークがどのようなデザインパターンを実装しているのか説明する.
これはMVVMアーキテクチャと呼ぶ.
モデルの定義
モデルは通常Dartクラスで,HTTPリクエストやデータのキャッシュ,プラグインのようなシステムリソースの管理といった低レベルのタスクを行う.
モデルは通常Flutterライブラリをインポートする必要はない.
例えば,HTTPクライアントを使ってカウンタ状態を読み込んだり更新したりするモデルである.
import 'package:http/http.dart';
class CounterData {
CounterData(this.count);
final int count;
}
class CounterModel {
Future<CounterData> loadCountFromServer() async {
final uri = Uri.parse('https://myfluttercounterapp.net/count');
final response = await get(uri);
if (response.statusCode != 200) {
throw ('Failed to update resource');
}
return CounterData(int.parse(response.body));
}
Future<CounterData> updateCountOnServer(int newCount) async {
// ...
}
}
このモデルはFlutterのコードを一切使わず,動作しているプラットフォームについても仮定しない.
唯一の仕事はHTTPクライアントを使ってカウントを取得したり更新したりすることである.
これにより,単体テストでモデルをMockやFakeで実装することができ,アプリの低レベルコンポーネントと,完全なアプリを構築するために必要な高レベルのUIコンポーネントの境界を明確に定義することができる.
CounterData
クラスはデータの構造を定義し,アプリケーションの真の「モデル」である.
通常,モデル層はアプリに必要なコア・アルゴリズムとデータ構造を担当する.
変更不可な値型を使用するなど,モデルを定義する他の方法に興味がある場合,pub.devのfreezedやbuild_collectionなどのパッケージをチェックすると良い.
ViewModelの定義
ViewModel
はViewからModelに繋げる.
これは,Viewから直接アクセスModelにアクセスするの防ぎ,データフローがモデルが変化するということを遵守する.
データフローはViewModel
によって扱われ,notifyListeners
を通じて何かが変更したことを知らせる.
ViewModel
はまるでキッチンとお客の間を取り持つレストランのウェイターのような働きを持つ.
import 'package:flutter/foundation.dart';
class CounterViewModel extends ChangeNotifier {
final CounterModel model;
int? count;
String? errorMessage;
CounterViewModel(this.model);
Future<void> init() async {
try {
count = (await model.loadCountFromServer()).count;
} catch (e) {
errorMessage = 'Could not initialize counter';
}
notifyListeners();
}
Future<void> increment() async {
final currentCount = count;
if (currentCount == null) {
throw('Not initialized');
}
try {
final incrementedCount = currentCount + 1;
await model.updateCountOnServer(incrementedCount);
count = incrementedCount;
} catch(e) {
errorMessage = 'Count not update count';
}
notifyListeners();
}
}
ViewModel
はModelからエラーを受け取ったときのためにerroeMessage
を保存する.
これはビューがランタイムエラーを扱わないことを防ぎ,クラッシュさせないようにする.
その代わり,errorMessage
はViewを通じてユーザフレンドリーのエラーメッセージを表示できる.
Viewの定義
ViewModel
はChangeNotifier
であるため,ViewModel
の参照を持つすべてのウィジェットはViewModel
のリスナーに通知が届いたら,Viewの再構築を行うためにListenerBuilder
を使う.
ListenableBuilder(
listenable: viewModel,
builder: (context, child) {
return Column(
children: [
if (viewModel.errorMessage != null)
Text(
'Error: ${viewModel.errorMessage}',
style: Theme.of(context)
.textTheme
.labelSmall
?.apply(color: Colors.red),
),
Text('Count: ${viewModel.count}'),
TextButton(
onPressed: () {
viewModel.increment();
},
child: Text('Increment'),
),
],
);
},
)
このパターンは,UI設計とModelを使った低レベル処理を分けることができるようになる.
もっと状態管理について学ぼう
https://docs.flutter.dev/data-and-backend/state-mgmt/options
https://fluttersamples.com/