Dart

64. FlutterのProviderパターンを3分で理解する

FlutterのProviderパターンを3分で理解する

Flutter 初心者にとって Provider の扱いはとても難しく思うはず。
僕も例外なく Flutter 学習初めの頃は Provider を見てもあまり魅力を感じなかった。

今回はその Provider について理解できるようにすることが主目的である。

【目次】

事前知識

Provider の書き方を理解する上で、前提知識といいますか経験値が必要な気がします。

  • Container, Column, Row, ListView を使ってレイアウトを組み立てられる
  • StatefulWidget の setState の使い方を理解している
  • Widget の分割のメリットを理解している

多分、これらまで理解していたら Provider で今よりもいい感じの設計ができる(はず)。

Providerとは

Provider の概念・理念の理解は itome さんのブログが一番分かりやすいと思う。

この記事を読めば Provider については理解できるはずだった。はずだったというのは僕が例外だったから。
多分、本当に初心者の方にとっては Provider の理解は難しいと思う。

Provider の特徴

完全に Providerについて理解していないので、勿論間違った解釈をしているかもしれない。
そのレベルですが、 Provider を使うことで得られるメリットは

  • StatefulWidget を使う必要がなくなる ( setState の更新は不要)
  • iOS でいうところの RxSwift の使い方に似ている
  • 末端の widget まで値を送る必要がなくなる

これらだと思っている。
それではこれを前提に Provider のついて学習していきたい。

サンプルコード

Provider のサンプルコードについては上記のページと同じコードを載せることになります。
最初は Flutter の初期コードをそのままカスタマイズするためです。

それでは Flutter の初期コードについてコメント文を削除した状態を載せます。

main.dart

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      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++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

こちらが初期コードになります。

特徴は

  • setState で値を更新している (StatefulWidget のため)
  • Widget の生成 と Model が同じクラス内で行われている

です。

これをProviderパターンに書き換えます。

Provider の手順について

それでは Provider を使う手順について紹介します。

  1. Provider をインストールする
  2. ChangeNotifier を継承した Model クラスを定義する
  3. 発火したいイベントを持っている widget にConsumer で包む
  4. Provider で値を更新させたい widget に ChangeNotifierProvider で包む
  5. 値を更新したい箇所 に Provider で包む

1. Provider をインストールする

Provider パッケージのページはこちらになります。

yaml ファイルに宣言してインストールします。

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  provider: ^4.1.2

使うファイルで import します。

main.dart

import 'package:provider/provider.dart';

これで準備が整いました。

2. ChangeNotifier を継承した Model クラスを定義する

次に ChangeNotifier を継承した Model クラスを定義します。

main.dart

import 'package:provider/provider.dart';

class CountModel extends ChangeNotifier {
  /// 初期値
  int count = 0;
  
  /// count の更新メソッド
  void increment() {
    count ++;
    notifyListeners();
  }
}

こんな感じでOKです。

3. 発火したいイベントを持っている widget に Consumer で包む

次に値を更新するイベントを持っているwidget を ChangeNotifierProvider で包んであげます。
ですが、最初のコードは StatefulWidget で書かれているので StatelessWidget に書き直して
Widget build(BuildContext context) の中身を移動させます。

main.dart

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

この時点では counter や incrementCounter が存在しないのでエラーになります。
まずは Scaffold の部分を次のように変更します。

child: Consumer<CountModel>(
          builder: (context, model, child) => Scaffold(
              appBar: AppBar(
                title: Text('Flutter Demo Home Page'),
              ),
             /*
             * 以下省略
              */
          )
      )

Consumer() で Scaffold を包みました。

4. Provider で値を通知したい widget に ChangeNotifierProvider で包む

次に値を更新させたい widget を ChangeNotifierProvider で包みます。
今回は MyHomePage クラスの Scaffold に対して値を通知したいです。

main.dart

return ChangeNotifierProvider<CountModel>(
      create: (context) => CountModel(),
      child: Consumer<CountModel>(
          builder: (context, model, child) => Scaffold(
              appBar: AppBar(
                title: Text('Flutter Demo Home Page'),
              ),
             /*
             * 以下省略
              */
          )
      ),
    );

5. 値を更新したい箇所 に Provider で包む

最後に更新された値を受け取ってそれを widget に反映させます。
値を提供するということで Provider と名付けられているかもしれません。

main.dart

Text(
    /// Provider を使う
    '${Provider.of<CountModel>(context).count}',
    style: Theme.of(context).textTheme.headline4,
    )

これで Consumer で包んだ FloatingActionButton をタップした時にProviderで受け取った Text の数値が変更されます。
またこれはwidget で切り離して使うこともできます。

class CountText extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text(
      /// context からModelの値が使える
      '${Provider.of<CountModel>(context).count}',
      style: Theme.of(context).textTheme.headline4,
    );
  }
}

なんと、model の count が Provider.of からシングルトンのように取得できるようになりました。
わざわざ count のプロパティを受け取る必要がないことがわかります。

これでビルドすると Flutter プロジェクトが作成された初期画面と同じ挙動になります。

全体のソースコード

最後に全体のソースコードを載せておきます。

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

class CountModel extends ChangeNotifier {
  /// 初期値
  int count = 0;

  /// count の更新メソッド
  void increment() {
    count++;
    notifyListeners();
  }
}

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(),
    );
  }
}


class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<CountModel>(
      create: (context) => CountModel(),
      child: Consumer<CountModel>(
          builder: (context, model, child) => Scaffold(
              appBar: AppBar(
                title: Text('Flutter Demo Home Page'),
              ),
              body: Center(
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    Text(
                      'You have pushed the button this many times:',
                    ),
                    CountText(),
                  ],
                ),
              ),
              floatingActionButton: FloatingActionButton(
                onPressed: model.increment,
                tooltip: 'Increment',
                child: Icon(Icons.add),
              )
          )
      ),
    );
  }
}

class CountText extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text(
      /// context からModelの値が使える
      '${Provider.of<CountModel>(context).count}',
      style: Theme.of(context).textTheme.headline4,
    );
  }
}

ということで長かったですが、最後にこのパターンを暗記します。
一回テンプレート的に書き方を覚えたら後は応用になるからです。

ちなみに、この Provider パターンは、

RxSwift に例えるとするなら、
Consumer は BehavorRelay の accept で値を渡すところ
Provider は Observable で値を通知するところ

iOS でいうならば、
Consumer は NotificationCenter をpostするところ
Provider は NotificationCenter でadd して値を通知するところ

みたいなイメージです。

そんな印象でした。今日はここまで。

それでは、バイバイ!

ABOUT ME
tamappe
都内で働くiOSアプリエンジニアのTamappeです。 当ブログではモバイルアプリの開発手法について紹介しています。メインはiOS、サブでFlutter, Android も対応できます。 執筆・講演のご相談は tamapppe@gmail.com までお問い合わせください。