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 を使う手順について紹介します。
- Provider をインストールする
- ChangeNotifier を継承した Model クラスを定義する
- 発火したいイベントを持っている widget にConsumer で包む
- Provider で値を更新させたい widget に ChangeNotifierProvider で包む
- 値を更新したい箇所 に 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 して値を通知するところ
みたいなイメージです。
そんな印象でした。今日はここまで。
それでは、バイバイ!








