LoginSignup
25
26

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-09-29

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

【Flutter】 戻るボタンで遷移元画面に値を渡す方法

ダウンロード.gif

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

Providerとは

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

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

状態管理について

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

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

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

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

Providerの登場

そこで、Providerの登場です。

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

GoogleもProviderパッケージの使用を推奨しており、今のところ状態管理を行うにはproviderを第一本線ととらえて問題ないでしょう。

Pragmatic State Management in Flutter (Google I/O'19)

Providerのメリット

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

今回の例では、Widgetの数も少ないですし、ほとんどのWidgetをConsumerの配下にしています。
必要な個所のみレンダリング対象としたケースはこちらを参考ください。

【Flutter】アプリ製作から学ぶProviderの使い方【図解付き】

Providerイメージ

Flutter-Provider-3 (1).png

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

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

provider実装手順

それでは、前回記事をProviderを使って実現してみましょう。

実装手順はこちら。

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

順に説明します。

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

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

pubspec.yaml
dependencies:
  provider: ^4.3.2+1

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

2.MaterialAppの上にChangeNotifierProviderを配置

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

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

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

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

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

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

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

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

class ResultProvider extends ChangeNotifier {
 ・・・
}

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

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

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がリビルド対象となります。

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

今回は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,
          ),
        );
      }),
    );

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

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

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

    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を使った感がありますが、一例と考えていただければ幸いです。

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のメソッド
              },
            ),
          ],
        ),
      );
    }

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

    // サンプル2: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配下のみ再描画されることを確認するために、
コード内のサンプル1とサンプル2を切り替えて実行してみてください。

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

参考

こちらでもうひとつProviderの使用例を示しています。

【Flutter】アプリ製作から学ぶProviderの使い方【図解付き】

25
26
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
26