今すぐ使いたい人のためのProviderの使い方【Flutter】


まえがき

こんにちは、高校生プログラマーのよしけんと申します。
この記事ではFlutter初学者に向けて「Provider」を使った状態管理の方法を解説します。

とはいっても、自分も初学者です。
最初はProviderを理解するのに大きな苦労をしました。
正直、まだ理解できていない部分も多くあります…
でも、理解は後回しにして、とにかく使えるようにはなりました。

というわけで、この記事は
今すぐ使いたい人のために、詳しい解説は抜きにして、実践的なProviderを学べる記事」となっております。
もっと本質的な理解をしたい方には、向いておりません。ご了承ください…

ではまず、状態管理について解説します。

状態管理とは?

状態管理はいろんな言い方をされます。
「UIを再描画するときの考え方」や「UIの状態を管理するときの考え方」などなど…

正直、まったくの初学者であった僕にとってはチンプンカンプンでした。

そして、最終的に僕がたどり着いた結論は「ソースコードを小分けするときの考え方」という意味です。
個人的にはこれが一番しっくりきます!

「ソースコードの小分け」とはどういうことか。もう少し解説します。

アプリケーションの基本的な動作は、主に「入力→処理→出力」です。

「めざまし時計アプリ」を例に挙げると、
起きる時刻の設定…入力
現在時刻の計算…処理
音を鳴らす…出力
といった感じ。だいぶ省略しましたが、なんとなくわかりますかね?

昔のBASICとかだと「手続き型プログラミング」といって、上から下へ流れるようにそれぞれの動作を一連のソースコードで書いていました。
でもそれだと不具合の箇所が見つけにくいし、機能も追加しにくいよね?

ということで、なんやかんやあって今では「オブジェクト指向型プログラミング」という方式がメジャーになっています。
オブジェクト指向では、先ほどのめざまし時計の「起きる時刻の設定」「現在時刻の計算」「音を鳴らす」といったそれぞれの動作を分割して、小分けにして書きます。

これだと、たとえば「音が出ない!」という不具合が起きたときは「音を鳴らす」動作の箇所だけ見れば、不具合を発見できます。

手続き型だと、場合によってはソースコード全体を見直す必要があったりしますが、オブジェクト指向ならある程度の手間を省けます。

で、状態管理の話に戻りますが、結局は「状態管理は、オブジェクト指向だ!」という理解で構いません。
状態管理をちゃんと学ぶことで、こうしたソースコードの分割をすることができ、より安全でより効率の良いプログラミングができます。

で、Providerとは数ある状態管理手法のうちの一つです。
GoogleもFlutterでの開発においてのスタンダードとして推奨しているので、これを使っていくべきです。

でもまだ漠然としていると思うので、とりあえず使っていきましょ!

早速、Providerを使っていこう

今回は、Providerを通してこんな画面のアプリを作っていきます。

+1ボタンを押すと中央の数字が1だけ増えます。
-1ボタンを押すと、1だけ減ります。超単純。

では、これをProviderで実装していきましょう!

まずは初期設定から

Providerを使用する前に、pubspec.yamlで設定する必要があります。

pubspec.yaml
dependencies:
  provider: ^4.3.3
  flutter:
    sdk: flutter

providerという項目を追加してください。
今回はバージョン4.3.3を使用しますが、できるだけ新しいバージョンにしたほうがいいと思いますので、その辺はお任せします。
その後、pub getを実行します。

ここをクリックすれば、OKです。

これでProviderを使用する準備ができました!

とりあえず、見よう見まねで書いてください

ではmain.dartに書いていきます。まずはサンプルコードを消去してください。
これからは、あまり深く考えすぎるとしんどいので、とりあえず僕の「見よう見まね」で書いていってください!

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

いつものmaterial.dartに加えて、provider.dartをインポートします。

その続き。

main.dart

class AAAAA with ChangeNotifier {
  int _result = 0; //増減の結果を保持する変数

  void Add() {  //1だけ加算するメソッド
    _result = _result + 1;
    notifyListeners();
  }

  void Min() {  //1だけ減算するメソッド
    _result = _result - 1;
    notifyListeners();
  }
}

早速Providerの要素が出てきました。
ここでは「AAAAAクラス」を、ChangeNotifierクラスを継承して書いています。
この、ChangeNotifierクラスを継承したAAAAAクラスのことを「状態クラス」なんて言ったりもします。
詳しい解説はもうちょっと後でするので、今はスルーで。

ここで見てほしいのはnotifyListeners()メソッドです。
Add()メソッドとMin()の中で使用していますが、変数の変化、つまり_result変数の中身が書き換わったことをアプリに知らせています。
これもあとで解説します!

その続き。

main.dart
void main() => runApp(
      ChangeNotifierProvider(
        create: (context) => AAAAA(),
        child: MaterialApp(
          home: MainApp(),
        ),
      ),
    );

main()エントリポイントですが、ここで注意。
必ずMaterialAppウィジェットより上にChangeNotifierProviderウィジェットを配置してください。
MaterialAppより下に配置してしまうと、うまく動かない可能性が大きいです。
とりあえず、そういうもんだと割り切ってください!

少し解説すると、ChangeNotifierProviderウィジェットでは先ほどの状態クラスの存在をアプリケーションに知らせるような役割を持っています。

その続き。

main.dart
class MainApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('ボタンで増減!'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
                Provider.of<AAAAA>(context)._result.toString(),
              style: TextStyle(
                fontSize: 80.0,
              ),
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                ElevatedButton(
                  child: Text('+1'),
                  onPressed: () {
                    Provider.of<AAAAA>(context, listen: false).Add();
                  },
                ),
                ElevatedButton(
                  child: Text('-1'),
                  onPressed: () {
                    Provider.of<AAAAA>(context, listen: false).Min();
                  },
                ),
              ],
            )
          ],
        ),
      ),
    );
  }
}

いっきにコピペしちゃってください。
先ほどのMaterialAppでMainAppウィジェットを指定しましたが、その中身です。

基本的にはいつもと変わりありませんが、重要なポイントがいくつかあります。


            Text(
                Provider.of<AAAAA>(context)._result.toString(),
              style: TextStyle(
                fontSize: 80.0,
              ),
            ),

まず、中央の数字であるTextウィジェットですが、ここでProvider.of(context)._resultと記述しています。
こうすることで、先ほどの状態クラスであるAAAAAクラスの_result変数にアクセスできます。

さきほど状態クラスのAdd()、Min()メソッド内でnotifyListener()を書いていましたが、
Provider.ofで呼び出した変数ははnotifyListener()が呼ばれるたびに更新されます。
つまり、いちいち値が更新されるたびにアクセスするような記述をしなくてもよくなります。

取得した_resultはint型なので、toString()でString型に変換しています。

                ElevatedButton(
                  child: Text('+1'),
                  onPressed: () {
                    Provider.of<AAAAA>(context, listen: false).Add();
                  },
                ),
                ElevatedButton(
                  child: Text('-1'),
                  onPressed: () {
                    Provider.of<AAAAA>(context, listen: false).Min();
                  },
                ),

続いて+1、-1ボタンですが、ここでも同じようにProvider.ofを使ってメソッドを呼び出しています。
Provider.of(context, listen: false).Add()でAddメソッドに、
Provider.of(context, listen: false).Min()でMinメソッドにそれぞれアクセスして、_result変数を増減させています。
こんな感じで、Provider.ofを使うことで変数だけでなくメソッドにもアクセスできます!

なお、Provider.of(context, listen: false)と、listenプロパティをfalseに設定していますが、
これは画面の再描画を防止しています。

実際、UIが変化するのは「中央の数字」だけでボタンは変化する必要がないので、ここでは再描画しないようにしています。
listenプロパティを省略するとデフォルトではtrueです。つまり、先ほどの「中央の数字」の部分で書いたProvider.ofはtrueです。

これだけ書けば、アプリが完成します。

Providerについてまとめると

①ChangeNotifierを継承して、状態クラスを作る
②状態クラスが持つ変数を更新するときはnotifyListener()を書く
③MaterialAppウィジェットより上に、ChangeNotifierProviderメソッドを置く
④Provider.ofで、状態クラスの変数にも関数にもアクセスできる
⑤notifyListener()が呼ばれると、Provider.ofで呼び出している変数は自動で更新される

この5点。

「入力→処理→出力」における「処理」の部分を、状態クラスの中だけで済ませるような設計にすることで、アプリケーションの安全性を高められます。

今回は実践的なProviderの使い方を解説しましたが、実践的すぎるあまり説明を大幅に省略しました。
ただ、一つだけ言いたいのは「使っていくうちに、慣れる!」ということ。

僕もはじめはProviderを使っても全然思い通りにアプリが動きませんでした。
でも、ほかの人のやり方をとりあえず見よう見まねで、理解を後回しにして学ぶことで、ある程度まで使いこなせるようになりました。

とりあえず、考えるよりやることが重要です。
あんまり理解していなくてもいいので、とりあえずこの記事のコードをコピペして動かしてみてほしいです。

いつか理解できるので、とりあえず手を動かしてみましょう!