Flutter 状態管理

# 元記事

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```は状態を保存する．
例えば，以下のウィジェットについて考えてみる．
```dart
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を構築するために使われる．
```dart
class MyCounter extends StatelessWidget {
  final int count;
  const MyCounter({super.key, required this.count});

  @override
  Widget build(BuildContext context) {
    return Text('$count');
  }
}
```

これにより，ウィジェットの他のユーザが，それを使用するために何を提供する必要があるかを知ることができる．

```dart
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```に依存するすべてのウィジェットは再構築される．
```dart
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()```メソッドを呼ぶ．
```dart
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```型は一つのパラメータを使って関数コールバックを宣言する．
```dart
typedef ValueChanged<T> = void Function(T value);
```

ウィジェットコンストラクタの中で```onChanged```を公開することによって，ウィジェットが```onChanged```を呼んだときに，このウィジェットを使用しているウィジェットが応答する方法を提供する．
```dart
class MyCounter extends StatefulWidget {
  const MyCounter({super.key, required this.onChanged});

  final ValueChanged<int> onChanged;

  @override
  State<MyCounter> createState() => _MyCounterState();
}
```

例えば，このウィジェットは```onPressed```コールバックを使って，状態が保持する```count```変数の値を更新する．
```dart
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```を呼びだす．

```dart
class CounterNotifier extends ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}
```

そして，それを```ListenableBuilder```に渡して，```ChangeNotifier```がリスナーを更新するたびに，ビルダー関数が返すサブツリーが再構築されるようにする．
```dart
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```のインスタンスを作成する．
```dart
ValueNotifier<int> counterNotifier = ValueNotifier(0);
```

変数を見たり更新したりするために```value```フィールドを使い，変数が更新されたことをListenerを通じて知らせる．
```ValueNotifier```は```ChangeNotifier```を拡張したものであるから，```Listenable```であり，```ListenableBuilder```を使える．
```builder```コールバックの中で値が提供される```ValueListenableBuilder```も使える．
```dart
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クライアントを使ってカウンタ状態を読み込んだり更新したりするモデルである．
```dart
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```はまるでキッチンとお客の間を取り持つレストランのウェイターのような働きを持つ．
```dart
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```を使う．

```dart
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/