FlutterのnotifyListenersとChangeNotifierProviderを整理する

---
# :book: Flutterの記事を整理し本にしました :book:
https://zenn.dev/kazutxt/books/flutter_practice_introduction

* <font color="red"><b>本稿の記事を含む様々な記事を体系的に整理し本にまとめました</b></font>
* 今後はこちらを最新化するため、<font color="red"><b>最新情報はこちらをご確認くださ</b>い</font>
* 10万文字を超える超大作になっています(笑)

---

# はじめに
* いままで何となくにしていたInheritedWidgetを学びなおしたので、整理して記事にしてみました。

## まとめ
* [Flutter系の記事のまとめ](https://qiita.com/kazutxt/items/524acb1c134015682ccf)

# ProviderとChangeNotifier
本チャプターは、下記のGoogle I/O'19の情報をベースとしています。
* https://www.youtube.com/watch?v=d_m5csmrf7I

`InheritedWidget`を使うと上位のWidgetの値を使えますが、`InheritedWidget`を直接使うことは少なく、`Provider`という便利なパッケージがあるので、こちらを使うことが多いです。

InheritedWidgetについて、よくわからないという方は、[InheritedWidget](https://zenn.dev/kazutxt/books/flutter_practice_introduction/viewer/development_expert3)をご参照ください

Providerは、InheritedWidgetラッパーライブラリです。
使う場合は、ライブラリの読み込みが必要です。

```yaml:pubspec.yaml
dependencies:
  provider: "^5.0.0"
```

使い方は`InheritedWidget`に似ており、`InheritedWidget`の代わりに、使うWidgetの上位に`Provider`を挟んでおき、下位Widgetでその値を使います。

前回の[InheritedWidget](https://zenn.dev/kazutxt/books/flutter_practice_introduction/viewer/development_expert3)との対比で確認してみます。

```dart:main.dart
import 'package:flutter/material.dart';
import 'package:hello_world/Widgets.dart';
import 'package:provider/provider.dart';
void main() {
  runApp(MyApp());
}
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}
class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, this.title}) : super(key: key);
  final String? title;
  @override
  _MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
    print("count:" + _counter.toString());
  }
  // Scaffoldの下のCenter部分を先に静的に作っておき、作り返さないように制御
  // 深い階層の伝播は証明できたためにシンプルにCenter->WidgetAに変更
  final Widget _widget = Center(child: WidgetA());
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title!),
      ),
-      //body: MyInheritedWidget(count: _counter, child: _widget),
+      body: Provider<int>.value(value: _counter, child: _widget),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}
```

```dart:WidgetA.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class WidgetA extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    int count;
    try {
-      //MyInheritedWidget _myInheritedWidget = MyInheritedWidget.of(context);
-      //count = _myInheritedWidget.count;
+      count = Provider.of<int>(context);
    } catch (e) {
      count = 0;
    }
    return Text("$count", style: TextStyle(fontSize: 100));
  }
}

```

ほぼ`InheritedWidget`が`Provider`に置き換わっただけであることが確認できます。

![pic4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/af909a74-fc43-8150-5bf4-7522938b336d.png)


## Consumer
前回のようにWidgetAを自作せずに、`Provider`パッケージが提供する`Consumer`を使うとより簡単に同じ機能を実現できます。

```dart:main.dart
//　_widgetの定義以外は同じのため省略
- final Widget _widget = Center(child: WidgetA());
+ final Widget _widget = Center(
+       child: Consumer<int>(
+     builder: (context, value, _) => Text(
+      value.toString(),
+      style: TextStyle(fontSize: 100),
+    ),
  ));
```

もちろん、再構築される範囲はこのConsumerの範囲のみです。

Providerにはいくつかの種類が提供されています。
この後紹介する変更を検知して動作する`ChangeNotifierProvider`や複数の`Provider`を取り扱える`multiProvider`などがあります


# ChangeNotifierとChangeNotifierProvider
`Provider`の上位Widgetの値の取得を発展させ、データが変わったことを通知する(`notifyListeners`)とデータが変わったことを知りWidgetを作り直す(`ChangeNotifierProvider`)を使って、状態管理を行うことができます。

以下のようなスライダーとその値を表示させるシンプルなアプリで動作を確認します。

![Screenshot_1618644532.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/e971c7d8-597c-39cf-5a20-ffa1249f90be.png)


```dart:MyData.dart
import 'package:flutter/foundation.dart';
class MyData with ChangeNotifier {
  double _value = 0.5;
  // getter
  double get value => _value;
  // setter
  set value(double value) {
    _value = value;
    notifyListeners(); //通知
  }
}
```
まず、データを保持し、変更されたら通知するクラスを作ります。
`ChangeNotifier`を`with`で`mixin`することで、通知を行う`notifyListeners`が使えるようになります。
この関数が呼ばれると、変更を監視している`Widget`に変更が通知されます。


```dart:Slider.dart
import 'package:flutter/material.dart';
import 'package:hello_world/mydata.dart';
import 'package:provider/provider.dart';
class MySlider extends StatefulWidget {
  @override
  createState() => _MySliderState();
}
class _MySliderState extends State<MySlider> {
  @override
  Widget build(BuildContext context) {
    final mydata = Provider.of<MyData>(context);
    return Slider(
        value: mydata.value, onChanged: (value) => mydata.value = value);
  }
}
```

次にスライダーです。
スライダーで設定した値をMyDataに設定します。
また、スライダの位置をvalueで設定するため、入力値としてMyDataの値の取得も行います。

```dart:main.dart
void main() { /* 変更なしのため省略 */}
class MyApp extends StatelessWidget { /* 変更なしのため省略 */}
class MyHomePage extends StatefulWidget { /* 変更なしのため省略 */}
class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (BuildContext context) => MyData(),
      child: Scaffold(
          appBar: AppBar(
            title: Text(widget.title!),
          ),
          body: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Consumer<MyData>(
                builder: (context, mydata, _) => Text(
                  mydata.value.toStringAsFixed(2),
                  style: TextStyle(fontSize: 100),
                ),
              ),
              MySlider(),
            ],
          )),
    );
  }
}

```
ChangeNotifierProviderを上位に挟み、変更された時にその変更を検知できるようになっています。


処理の流れは下記のとおりです。
1. SliderはProvider.ofでMyDataにアクセスし、値の取得や設定を行う
2. MyDataのsetterの中で、notifyListenersでリスナーに変更を通知する
3. Consumerが変更を検知して、Textへの値の設定と作り直しを行う




## context.select/context.read

下記は、BuildContextのextensionとして、コードをシンプルにするために役立ちます。

* context.select(変更を監視する)
* context.read(変更を監視しない)

```dart:Slider.dart
import 'package:flutter/material.dart';
import 'package:hello_world/mydata.dart';
import 'package:provider/provider.dart';
class MySlider extends StatefulWidget {
  @override
  createState() => _MySliderState();
}
class _MySliderState extends State<MySlider> {
  @override
  Widget build(BuildContext context) {
    // context.select,readを使ってアクセス
    return Slider(
        value: context.select((MyData mydata) => mydata.value),
        onChanged: (value) => context.read<MyData>().value = value);
  }
}
```

```dart:main.dart
void main() { /* 変更なしのため省略 */}
class MyApp extends StatelessWidget { /* 変更なしのため省略 */}
class MyHomePage extends StatefulWidget { /* 変更なしのため省略 */}
class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (BuildContext context) => MyData(),
      child: Scaffold(
          appBar: AppBar(
            title: Text(widget.title!),
          ),
          body: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Consumer<MyData>(
                  // context.readを使ってアクセス
                  builder: (context, schedule, _) => Text(
                      context.select(
                          (MyData mydata) => mydata.value.toStringAsFixed(2)),
                      style: TextStyle(fontSize: 100))),
              MySlider()
            ],
          )),
    );
  }
}
```


Providerパッケージをより改善したRiverpodパッケージもありますが、今回は取り扱いません。
「グローバルでの定義が可能」「ランタイムエラーを起こさない」「同じ型での複数探索が可能」など改良が加えられています。

# Providerデザインパターン
本チャプターで見てきた内容はProviderデザインパターンと呼ばれるものです。

[開発の中級4:StreamとBLoCデザインパターン](https://zenn.dev/kazutxt/books/flutter_practice_introduction/viewer/development_advanced4)では、Streamというものを用いて、入力、ビジネスロジック、出力の3つを分離するデザイパターンを紹介しました。

こちらは、Streamを使ってビジネスロジックを分離する考え方でしたが、Providerデザインパターンは、データの変更をnotifyListnersで通知し、ChangeNotifierProviderでその変更を検知することで、入出力とビジネスロジックを分離します。

整理すると、下記のようになります。

![pic7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/35d3f1e1-31dd-3658-b65b-56f572914643.png)


上記の整理は基本的な考え方に基づくもので、現実的には組み合わせたりプロジェクトの案件に合わせてカスタマイズされることが多くあります。