【Flutter】StatefulWidgetを使わずにProviderを使って効率的にWidgetの再描画をする

今回はProviderというライブラリを使ってFlutterのWidgetを状態管理をしてみようと思います。

状態管理とは、変数や画面に変更があった際にそれを検知する仕組みと考えるといいと思います。

例えば画面に表示しているテキストを更新したい時は、変更後のテキストをアプリに伝えてそれ表示させるように画面を再描画する必要があります。

つまり状態管理はアプリの基本的な機能の一つと言えます。

Fluttrerデフォルトの状態管理方法としてStatefulWidgetが存在しますが、こちらは画面のほんの些細なWidgetの状態を更新しようとした時にも画面全てのWidgetを更新してしまうという点でアプリのパフォーマンス面でベストな方法とはあまり言えません。

その問題点を解決してくれるのが今回紹介するProviderというライブラリになります。

今回はそんなProviderの基本的な機能である

  1. 画面描画
  2. データの受け渡し

の二点にフォーカスして解説していきます。

これはGoogle先生が公式に推奨している状態管理方法なのでこの機会に覚えてみてください。

Providerのメリット

Providerで状態管理するとなにがいいのか大きくざっくり2つについて説明します。

UIとロジックを別ファイルで分離して管理できる

Providerを使うときはUIを表示するファイルと裏側のロジックを管理するmodelファイルに分けたファイル構成が可能になります。

そのため、役割の棲み分けが自然にできて可読性とメンテナンス性が上がります。

再描画するスコープを制限できる

StatefulWidgetではsetStateメソッドを使用するとそのclass内にあるbuild関数以内全てのWidgetを再描画することになります。

しかしProviderを使用することによって再描画したい範囲を自分で絞って指定することができて無駄な再描画を削減することが可能です。

状態が変化することがない固定のテキストやボタンにまで再描画をかける必要性はないですからね。

Providerのインストール

pubspec.yamlに以下の記述をして、flutter pub getコマンドをターミナルで叩きましょう。

pubspec.yaml
ependencies:
  flutter:
    sdk: flutter
  provider: ^4.3.3 <= 追加

画面描画

先に全体コードを載せておきます。とりあえずコピペして動かしてみてください。

ポイントになる箇所はコード内にコメントしてあります。

main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'main_model.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'provider demo',
      // Providerで状態管理する箇所を囲む
      home: ChangeNotifierProvider<MainModel>(
        // 画面が読み込まれた時にMainModelも読み込ませる
        create: (_) => MainModel(),
        child: Scaffold(
          appBar: AppBar(
            title: Text('provider'),
          ),
          // 再描画したい箇所だけConsumerで囲む
          // notifyListeners()で再描画される
          // modelの中身はMainModel
          body: Consumer<MainModel>(builder: (context, model, child) {
            return Center(
              child: Column(
                children: [
                  Text(model.text),
                  ElevatedButton(
                    child: Text('ボタン'),
                    onPressed: () {
                      model.changeText();
                    },
                  )
                ],
              ),
            );
          }),
        ),
      ),
    );
  }
}
main_model.dart
import 'package:flutter/material.dart';

class MainModel extends ChangeNotifier {
  String text = 'テキスト';

  void changeText() {
    text = 'テキストが変わった';
    notifyListeners();
  }
}

Providerを使うときは基本1つのUIを表示するファイルに1つの状態管理用のファイルをセットにして使用します。

  1. UIを構築するWidgetのみを記述するmain.dart
  2. 変数やメソッドなどの直接UIに関係しない、裏で保持するデータを記述するmain_model.dart

と言った形で記述を分けていきます。

重要なポイントをピックアップして説明します。

main.dart
...
// Providerで状態管理する箇所を囲む
      home: ChangeNotifierProvider<MainModel>(
        // この画面が作成されたときにMainModelも読み込ませる
        create: (_) => MainModel(),
        child: Scaffold(
          appBar: AppBar(
            title: Text('provider'),
...

まずはmain.dartのProviderで状態管理したい箇所をラップします。今回はScaffold全体をChangeNotifierProviderでラップしています。

そうする事で、Scaffold配下のWidgetがProviderで状態管理できるようになりました。

その下にあるcreateにはmain.dartとセットのmodelを記述してるファイルを指定します。

これでmain.dartが読み込まれた際に一緒にmain_model.dartも読み込まれて、記述されている変数やメソッドにアクセスできるようになります。

main.dart
body: Consumer<MainModel>(builder: (context, model, child) {
            return Center(
              child: Column(
                children: [
                  Text(model.text),
                  ElevatedButton(
                    child: Text('ボタン'),
                    onPressed: () {
                      model.changeText();
                    },
                  )
                ],
              ),
            );
          }),

main_model.dartに記述されている変数やメソッド等はConsumerのbuildの引数になっているmodelに格納されているのでアクセスするには変数の頭に「model.」をつけましょう。

main_model.dart
void changeText() {
    text = 'テキストが変わった';
    notifyListeners();
  }

ElevatedButtonをタップするとonPressedプロパティ内でmodelファイルのchangeTextメソッドが起動されます。この際に画面に表示する変数であるtextに先程と違う文言を代入しています。

その次のnotifyListenersメソッドですが、これはChangeNotifierProviderで囲んだ部分のWidgetを再構築するためのメソッドです。

StatefulWidgetのsetStateみたいなものだと思ってください。

notifyListenersを呼ぶと、Consumer<MainModel>が発火を検知して自動でWidgetを再構築します。

これがProviderの基本的な状態管理方法になります。覚えてみると結構ソース管理が楽になったと実感するはずです。

データの受け渡し

Providerは状態管理用のライブラリなので、他classやWidgetに変更後のデータを伝える方法も用意されています。

データの受け渡し方法はいくつかあるのですが、今回はよく使う方法二つを解説します。

Provider.value

Providerでのデータ受け渡しで最もベーシックな方法がProvider.valueを使った方法です。

渡したいデータが一つのみの場合に使用します。

Provider.value側ではで渡したいデータと渡したい宛先を指定することでデータを渡します。

Provider.of側ではProvider.valueで渡されたデータを受け取ります。この時に受取るデータの型を指定してあげる必要がある点に注意してください。

parent.dart
class Parent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Provider<String>.value(
      value: 'データ',  // 渡したいデータを指定
      child: Child()  //  渡したい宛先
    )
  }
}
child.dart
class Child extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text(
      Provider.of<String>(context)  // データをProvider.valueから受け取る
    );
  }
}

MultiProvider

送りたいデータが複数ある場合にはMultiProviderを使用します。

Provider.valueをMultiProviderでラップして複数扱えるようにしているだけなので使用感はProvider.valueと同じです。

注意点としては送れる同じ型は1つまでという点です。

例えばString型を既にProvider.valueで指定している場合はもうString型は指定できません。

parent.dart
class Parent extends StatelessWidget {
  @override
    MultiProvider(
      providers: [
        Provider<String>.value(value: 'データ'),
        Provider<Int>.value(value: 1000),
        Provider<List>.value(value: ['データ1', 'データ2']),
      ],
      child: Child(),
    )
  }
}
child.dart
class Child extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children:[
        Provider.of<String>(context)
        Provider.of<Int>(context).toString
        Provider.of<List>(context).toString
      ]
    );
  }
}

おわりに

いまいまの状態管理はProviderで管理することが望ましいとされていますが、現在その後継となるRiverpodというライブラリも公開されています。

ただこちらはまだ性能として不安定な箇所が多い印象ですが、機能としてはほぼProviderの上位互換に近い性能をしているので近い将来状態管理パッケージはこちらに移行してくる開発者も増加するでしょう。

この機会にriverpodの今後に期待しつつ、Providerの基本を習得してみてくださいー。

Flutterのおすすめ技術書については「【2022年最新】現役エンジニアがFlutter初学者向けおすすめ技術書3選を紹介!」で紹介しているので良ければ参考にしてください。

【2022年最新】現役エンジニアがFlutter初学者向けおすすめ技術書3選を紹介!

おわり

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です