【Flutter】Providerを使って複数画面で再描画を行う【初心者向け】


前回記事では、StatefulWidgetを使用して異なる画面から受け取った値で状態更新を行い、画面の再描画（リビルド）を実現してみました。

>[【Flutter】 戻るボタンで遷移元画面に値を渡す方法](https://qiita.com/mamoru_takami/items/f53aebd97fb1140d122a)

![ダウンロード.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/699790/e84d7631-6b78-3ffe-9ae2-479c37cd2722.gif)

今回は、Providerを使用してそれを実現してみたいと思います。

## Providerとは

Providerとは、「状態管理」に使われるパッケージのこと。

ざっくりと説明しますが知っている方は飛ばしてください。

### 状態管理について

Flutterには、データに変更があった際に画面を再描画する仕組みがあります。
また、そのデータのことを「**状態**」と呼んだりします。

これまでは、`StatefulWidget`を使えば画面の再描画（リビルド）が実現できておりました。

しかし、`StatefulWidget`はそのクラス内でUIを構築する処理と状態管理する処理を記述する必要があります。

したがって、各画面で状態を共有したい場合には向いていません。

### Providerの登場

そこで、Providerの登場です。

Providerを使えば状態管理する処理を分けれるので、状態の共有がシンプルに実現できます。

GoogleもProviderパッケージの使用を推奨しており、今のところ状態管理を行うにはproviderを第一本線ととらえて問題ないでしょう。
> [Pragmatic State Management in Flutter (Google I/O'19)](https://www.youtube.com/watch?v=d_m5csmrf7I)


### Providerのメリット

- 再描画する範囲を限定的にできる<br>→表示速度の向上が期待できる
- UIを構築する処理と状態を更新する処理を分けれる
- ページ間のWidgetでも状態を簡単に共有できる
- わりと複雑なWidget構成にも対応できる


今回の例では、Widgetの数も少ないですし、ほとんどのWidgetを`Consumer`の配下にしています。
必要な個所のみレンダリング対象としたケースはこちらを参考ください。
> [【Flutter】アプリ製作から学ぶProviderの使い方【図解付き】](https://qiita.com/mamoru_takami/items/d12cba1e274721cf8848)

## Providerイメージ

![Flutter-Provider-3 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/699790/c76ede7b-8300-742e-44bd-a3182ed725ff.png)

図は正確でないかもですが、イメージはこんな感じ。

まずは、状態の変更と通知を行うクラスを準備。
(1)そのクラスのインスタンスより状態を更新したら、
(2)`notifyListeners()`によりProviderに変更を通知。
(3)再描画対象のWidgetのみリビルドされて、画面が更新されます。

## provider実装手順

それでは、[前回記事](https://qiita.com/mamoru_takami/items/f53aebd97fb1140d122a)をProviderを使って実現してみましょう。

実装手順はこちら。

1. `Provider`パッケージをインストール
2. `MaterialApp`の上に`ChangeNotifierProvider`を配置
3. `changeNotifier`クラスを拡張したクラスを作成
4. 状態を更新したいところで`notifyListeners()`を実行
5. 再描画対象としたいWidgetを`Consumer`でラップする

順に説明します。

### 1.Providerパッケージをインストール

`pubspec.yaml` に以下を追加して、`$ flutter pub get`を実行。

```dart:pubspec.yaml
dependencies:
  provider: ^4.3.2+1
```

バージョンはこちらで確認ください。
https://pub.dev/packages/provider

### 2.MaterialAppの上にChangeNotifierProviderを配置

Providerとして登録するクラスを、`ChangeNotifierProvider`というのを使って以下のような形で登録します。

```dart:
    MultiProvider(
      providers: [
        ChangeNotifierProvider<プロバイダー名>(
          create: (context) => プロバイダー名(),
        ),
      ],
   ),
```

Providerの数が複数ある場合は、`Providers:[]`内に追加していきましょう。

今回は`ResultProvider`というクラスをProviderとして登録します。
コードは以下のとおり。

```dart
void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider<ResultProvider>(
          create: (context) => ResultProvider(),
        ),
      ],
      child: MaterialApp(
        home: MyHomePage(),
      ),
    ),
  );
}
```

`ChangeNotifierProvider`の配置箇所は、状態更新の通知を受け取りたいWidgetより親の階層であれば大丈夫です。
ただ、上図コードで示したように`runApp`のど頭（`MaterialApp`の上）で宣言しておけば、後で使用箇所を変更したいときなどは修正不要なので楽ですね。

### 3. changeNotifierクラスを拡張したクラスを作成

`changeNotifier`クラスを拡張した`ResultProvider`クラスを作成します。

```dart:
class ResultProvider extends ChangeNotifier {
 ・・・
}
```

### 4. 状態を更新したいところでnotifyListeners()を実行

`notifyListeners()`を実行すると、Providerを介して、次手順で示す`Consumer`配下のWidgetがリビルドされます。

```dart:
class ResultProvider extends ChangeNotifier {
  String _result;

  ResultProvider() {
    initValue();
  }

  // 初期化
  void initValue() {
    this._result = "遷移先に移動";
  }

  void refresh() {
    initValue();
    notifyListeners(); // Providerを介してConsumer配下のWidgetがリビルドされる
  }

  void updateText(String str) {
    _result = str;
  }

  void notify() {
    notifyListeners(); // Providerを介してConsumer配下のWidgetがリビルドされる
  }
}
```





### 5. 再描画対象としたいWidgetをConsumerでラップする

`Consumer`を配置すると、`Consumer`配下のWidgetがリビルド対象となります。

```dart
Consumer<ResultProvider>(builder: (context, model, child) { 
// changeNotifier()実行で、この中のWidgetが再描画される
}
```

今回はappBar以外の遷移元ページや遷移先ページで使用しています。

```dart
    return Scaffold(
      appBar: appBar(),
      body: Consumer<ResultProvider>(builder: (context, model, _) {
        return _renderText(model);
      }),
      floatingActionButton:
          Consumer<ResultProvider>(builder: (context, model, _) {
        return FloatingActionButton(
          onPressed: () {
            model.refresh();  // ResultProviderのメソッド
          },
          child: Icon(
            Icons.refresh,
          ),
        );
      }),
    );
```

`builder`の第二引数`model`は、ChangeNotifierクラスを拡張したProviderクラスのインスタンスとなります。
この`model`を使用して、Provideクラスのメソッドやプロパティ（状態）の参照を行います。

なお、`model`は自由に名称変更可能です。

小分けにした関数`_renderText()`内はこんな感じ。

```dart
    Widget _renderText(ResultProvider model) {
      // print('text:${model._result}');
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              model._result, // ResultProviderのプロパティ
              style: Theme.of(context).textTheme.headline5,
            ),
            RaisedButton(
              child: Text('Go to Edit Page'),
              onPressed: () async {
                model.updateText('Hello! from HomePage.');
                await Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) =>
                        // 引数に遷移元から遷移先へ渡す値を設定
                        EditPage(),
                  ),
                );
                // print(result);
                model.notify(); // ResultProviderのメソッド
              },
            ),
          ],
        ),
      );
    }
```

##サンプルコード全文

今回のサンプルコード全文です。

前回記事のStatefulWidgetでは、Navigatorで値を渡しました。
今回はProviderのインスタンス`model`で状態を更新したり、受け取ったりしています。

ちょっと無理やりProviderを使った感がありますが、一例と考えていただければ幸いです。

```dart:main.dart

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:provider/provider.dart';

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider<ResultProvider>(
          create: (context) => ResultProvider(),
        ),
      ],
      child: MaterialApp(
        home: MyHomePage(),
      ),
    ),
  );
}

class ResultProvider extends ChangeNotifier {
  String _result;

  ResultProvider() {
    initValue();
  }

  // 初期化
  void initValue() {
    this._result = "遷移先に移動";
  }

  void refresh() {
    initValue();
    notifyListeners(); // Providerを介してConsumer配下のWidgetがリビルドされる
  }

  void updateText(String str) {
    _result = str;
  }

  void notify() {
    notifyListeners(); // Providerを介してConsumer配下のWidgetがリビルドされる
  }
}

// 遷移元ページ
class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // テキスト表示
    Widget _renderText(ResultProvider model) {
      // print('text:${model._result}');
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              model._result, // ResultProviderのプロパティ
              style: Theme.of(context).textTheme.headline5,
            ),
            RaisedButton(
              child: Text('Go to Edit Page'),
              onPressed: () async {
                model.updateText('Hello! from HomePage.');
                await Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) =>
                        // 引数に遷移元から遷移先へ渡す値を設定
                        EditPage(),
                  ),
                );
                // print(result);
                model.notify(); // ResultProviderのメソッド
              },
            ),
          ],
        ),
      );
    }

    // サンプル１：Scaffold全体をリビルド
    // return Consumer<ResultProvider>(builder: (context, model, _) {
    //   return Scaffold(
    //     appBar: appBar(),
    //     body: _renderText(model),
    //     floatingActionButton: FloatingActionButton(
    //       onPressed: () {
    //         model.refresh();
    //       },
    //       child: Icon(
    //         Icons.refresh,
    //       ),
    //     ),
    //   );
    // });

    // サンプル２：appBar以外をリビルド
    return Scaffold(
      appBar: appBar(),
      body: Consumer<ResultProvider>(builder: (context, model, _) {
        return _renderText(model);
      }),
      floatingActionButton:
          Consumer<ResultProvider>(builder: (context, model, _) {
        return FloatingActionButton(
          onPressed: () {
            model.refresh(); // ResultProviderのメソッド
          },
          child: Icon(
            Icons.refresh,
          ),
        );
      }),
    );
  }

  appBar() {
    print('appBar実行');
    return AppBar(
      title: Text('My Home Page（遷移元）'),
    );
  }
}

// 遷移先ページ
class EditPage extends StatelessWidget {
  final receive;
  const EditPage({Key key, this.receive}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Consumer<ResultProvider>(
      builder: (context, model, child) {
        return WillPopScope(
          onWillPop: () {
            model.updateText('Thank you! from 戻るアイコン'); // ResultProviderのメソッド
            Navigator.pop(context);
            return Future.value(false);
          },
          child: Scaffold(
            appBar: AppBar(
              title: Text('Edit Page（遷移先）'),
            ),
            body: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text(
                    model._result,
                    style: Theme.of(context).textTheme.headline5,
                  ),
                  RaisedButton(
                      child: Text('Return'),
                      onPressed: () {
                        model.updateText(
                            'Thank you! from 戻るボタン'); // ResultProviderのメソッド
                        Navigator.pop(context);
                      }),
                ],
              ),
            ),
          ),
        );
      },
    );
  }
}

```

`notifyListeners()`を実行した際、Consumer配下のみ再描画されることを確認するために、
コード内のサンプル１とサンプル２を切り替えて実行してみてください。

サンプル１では、`print('appBar実行');`が実行、出力されますが、サンプル２ではそうならないはずです。

## 参考

こちらでもうひとつProviderの使用例を示しています。
> [【Flutter】アプリ製作から学ぶProviderの使い方【図解付き】](https://qiita.com/mamoru_takami/items/d12cba1e274721cf8848)