Roadmap.shで学ぶFlutter - State Management -

# 導入

こんにちは、もんすんです。

[roadmap.shのFlutter](https://roadmap.sh/flutter)の「StateManagement」の章です。
状態管理に関する内容の学習になります。
日本では、かなり主力になっている`Riverpod`に関しては、今回の記事では割愛します。
代わりに`ChangeNotifier`と`ValueNotifier`をメインで進めていきます。

## **状態管理**

Flutterにおける状態管理とは、Flutterアプリケーションの<font color="red">データや状態を管理・更新するプロセス</font>を指します。
Flutterでは、ユーザーがアプリケーションとやり取りをする際などに、ウィジェットの状態が動的に変わることがあります。
F状態管理の手法には次のようなものがあります。

- **ScopedModel**: 状態を集中管理するためのモデルを使用するサードパーティ製の状態管理ソリューションです。
- **Provider**: 最小限のボイラープレートコードでウィジェットが状態にアクセスできるようにする軽量なソリューションです。
- **BLoC (Business Logic Component)**: ストリームとリアクティブプログラミングを用いて状態を管理する手法です。
- **Redux**: ReactのReduxライブラリに触発された状態管理ソリューションです。
- **InheritedWidget**: 状態をウィジェットツリーに渡すことができる組み込みのウィジェットです。

どの状態管理手法を選ぶかは、プロジェクトの複雑さや規模によって異なります。
小規模なプロジェクトでは、ProviderやInheritedWidgetで十分かもしれませんが、大規模なプロジェクトでは、ScopedModelやReduxのようなより強力なソリューションが必要になることがあります。

## ChangeNotifierクラス

FlutterのChangeNotifierは、Flutterにおける状態管理の基本クラスです。
これを使用することで、開発者はデータの変更を監視し、リスナーに通知することができ、ユーザーインターフェイスを効率的に更新することが可能になります。

ChangeNotifierを拡張することで、開発者は特定の状態やデータモデルを表すカスタムクラスを作成できます。
また、ChangeNotifierで状態を管理し、UIを動的に更新することで、ユーザー体験が向上します。
これにより、状態管理が簡素化され、インタラクティブなFlutterアプリケーションの作成が可能になります。

**参考**
https://api.flutter.dev/flutter/foundation/ChangeNotifier-class.html
 
ChangeNotifierは、VoidCallbackを使って通知するための変更通知APIを提供するクラスで、拡張またはミックスインとして使用できます。
リスナーの追加はO(1)、リスナーの削除と通知の送信はO(N)です。
ここでNはリスナーの数です。

**データモデルにChangeNotifierサブクラスを使用する**

データ構造がChangeNotifierを拡張またはミックスインすることで、Listenableインターフェースを実装し、ListenableBuilderなどのListenablesの変更を監視するウィジェットで利用できるようになります。

以下は、ListenableBuilderを使って、カウントを含むTextウィジェットの再ビルドを制限するシンプルなカウンターを実装した例です。

現在のカウントはChangeNotifierサブクラスに格納され、値が変更されるとListenableBuilderの内容が再ビルドされます。

```dart
import 'package:flutter/material.dart';

void main() {
  runApp(const ListenableBuilderExample());
}

class CounterModel with ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count += 1;
    notifyListeners();
  }
}

class ListenableBuilderExample extends StatefulWidget {
  const ListenableBuilderExample({super.key});

  @override
  State<ListenableBuilderExample> createState() =>
      _ListenableBuilderExampleState();
}

class _ListenableBuilderExampleState extends State<ListenableBuilderExample> {
  final CounterModel _counter = CounterModel();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('ListenableBuilder Example')),
        body: CounterBody(counterNotifier: _counter),
        floatingActionButton: FloatingActionButton(
          onPressed: _counter.increment,
          child: const Icon(Icons.add),
        ),
      ),
    );
  }
}

class CounterBody extends StatelessWidget {
  const CounterBody({super.key, required this.counterNotifier});

  final CounterModel counterNotifier;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          const Text('Current counter value:'),
          ListenableBuilder(
            listenable: counterNotifier,
            builder: (BuildContext context, Widget? child) {
              return Text('${counterNotifier.count}');
            },
          ),
        ],
      ),
    );
  }
}

```

この場合、ChangeNotifierのサブクラスはリストをカプセル化しており、リストにアイテムが追加されるたびにクライアントに通知します。

## ValueNotifier
FlutterのValueNotifierも、Flutterでの状態管理によく使われます。
ValueNotifierは、`ChangeNotifier`を拡張したクラスで、単一の値を効率的に管理し、その変更をリスナーに通知します。

ValueNotifierを使用することで、開発者はカウンターやユーザー入力などの特定のデータを簡単に追跡し、更新することができます。
これにより、状態管理が簡略化され、動的なFlutterインターフェースの実現が可能になります。

**参考**

https://medium.com/@avnishnishad/flutter-communication-between-widgets-using-valuenotifier-and-valuelistenablebuilder-b51ef627a58b

:::note info
ValueNotifierは、`ValueListenableBuilder`を使用してそれをリッスンしているウィジェットだけを再ビルドするため、パフォーマンス効率が高いらしいです。
:::

### ValueNotifierの作成

`ValueNotifier`は簡単に作成できます。新しいファイル`value_notifiers.dart`を作成し、すべての`ValueNotifier`を一つのファイルにまとめます。`ValueNotifier`は、`int`、`String`、`double`、`boolean`型のデータを保持できますし、自分のデータ型(クラスを使って)も使用できます。

```dart
import 'package:flutter/material.dart';
ValueNotifier<int> buttonClickedTimes = ValueNotifier(0);
```

**アプリのシナリオ**: ボタンがクリックされた回数をカウントするアプリを作成しています。`ValueNotifier`の使い方を理解するために、ボタンのための独立したStatelessウィジェットクラスを作成し、カウンターを表示するためにStatelessウィジェットを親ウィジェットとして使います。

### ValueNotifierの値を変更する

ボタンウィジェット内で、`ValueNotifier`の値をインクリメントします。

```dart
// value_notifiers.dartをインポートし忘れないでください。さもないとエラーが発生します。
GestureDetector(
  onTap: () => buttonClickedTimes.value = buttonClickedTimes.value + 1,
  child: Text("Button", style: Theme.of(context).textTheme.button),
);
```

### ValueListenableBuilder

`ValueListenableBuilder`は、`ValueNotifier`にアクセスし、`ValueNotifier`の値に依存するUIの部分を再ビルドするために使用されるウィジェットです。

親ウィジェット内で、`Text`ウィジェットを`ValueListenableBuilder`で囲みます。

```dart
ValueListenableBuilder(
  valueListenable: buttonClickedTimes,
  builder: (BuildContext context, int counterValue, Widget child) {
    return Text("Counter: $counterValue");
  },
)
```

`builder:(@required BuildContext context, @required int counterValue, Widget child)`は、`ValueNotifier`の値が変更されるたびにUIを再ビルドします。

### ネストされたValueListenableBuilder

ウィジェットが複数の`ValueNotifier`に依存している場合、ネストされた`ValueListenableBuilder`を使用します。

```dart
ValueListenableBuilder(
  valueListenable: buttonClickedTimes,
  builder: (BuildContext context, int counterValue, Widget child) {
    return ValueListenableBuilder(
      valueListenable: textColor,
      builder: (context, textColorValue, child) {
        return Text(
          "Counter: $counterValue",
          style: TextStyle(color: textColorValue),
        );
      },
    );
  },
)
```

### ValueNotifiersの破棄

使用されなくなった`ValueNotifier`を破棄することは、メモリの損失を防ぐための良い習慣です。

```dart
@override
void dispose() {
  buttonClickedTimes.dispose();
  textColor.dispose();
  super.dispose();
}
```

### Optional Parameter Child

`child`パラメータはオプションですが、`ValueNotifier`と`ValueListenableBuilder`をさらにパフォーマンス効率よく使用するために役立ちます。

```dart
ValueListenableBuilder(
  valueListenable: ColorNotifier,
  builder: (BuildContext context, int colorValue, Widget child) {
    return Container(
      height: 100,
      width: 100,
      color: colorValue,
      child: child,
    );
  },
  child: Text("button", textAlign: TextAlign.center),
)
```

上記のコードでは、`Container`は`colorNotifier`を使用して色を変更していますが、`Text`ウィジェットは`colorNotifier`の値に関係なく同じ値を持っています。しかし、`Container`の子であるため、`Container`が再ビルドされると`Text`ウィジェットも再ビルドされます。この問題に対処するために、オプションの`child`パラメータが役立ちます。

# 終わりに

今回は、Flutterにおける状態管理の基本概念と、`ChangeNotifier`および`ValueNotifier`の使用方法について詳しく解説しました。

状態管理は、アプリケーションの規模や要件に応じて適切な方法を選択することが重要です。

この記事を参考に、プロジェクトに最適な方法を見つけてみてください！