【Flutter入門】FlutterのProviderについて、めちゃくちゃ易しく解説してみる

Flutterを独学で始め、個人アプリのリリースまで至ったのだが、その中でProviderを使うのが一つのハードルだった。

Flutter開発において、初心者→中級者にステップアップする上で、Providerを使うというのは避けては通れないという思いがあった。
(Providerが使えれば自分も胸を張ってFlutterやってます！と言えるのでは？と思っている)

ただ、いざProviderを導入しようとすると、はじめはその概念やメリットを理解するのが難しかった。
(今でもちゃんと使いこなせている自信はないが・・・)

ということで、開発がいったん落ち着いたこのタイミングで、一度自分の理解を整理するためにも、記事にまとめておく。

### この記事のターゲット
・ProviderやBLoCパターンに興味があるが、よくわかっていない方
・バリバリ使いこなしている方(レビューをいただけると幸いです。)

## Providerでできること
そもそも、Providerを利用するメリットは何か？
**「よくわからないけど、使えるとカッコいい！」**
5ヶ月前の僕はそう答えると思う。

では、現在の僕はなんと答えるか？
小難しい内容は、他にたくさん良い記事があるので(「flutter provider」で検索)そちらに任せて、こう答える。

**「Widgetを分けて書くことができる」**

この記事で解説するのはこれだけ。
この一点に絞って、わかりやすく解説する。

ちなみにProviderを使うと、`StatefulWidget`を使わず`StatelessWidget`で書くことができる。

これにより、無駄な再描画を避けることができ、パフォーマンスが向上するというメリットもあるが、開発側がそのメリットを実感するのは難しいと思うので、本記事では取り上げない。

興味がある場合は、↓の素晴らしい記事が参考になる。
https://medium.com/flutter-jp/state-performance-7a5f67d62edd

とにかく、`StatefulWidget`を使わず`StatelessWidget`で書くということだけ、念頭において読み進めてもらいたい。


## Providerを使って、StatelessWidgetで書いてみる
**「Widgetを分けて書くことができる」**
このメリットや実際の使い方を解説する前に、まずはProviderがどんなものなのか？
`Provider+StatelessWidget`のコードを見て、ざっくり把握してもらいたい。

と言っても、いきなりそれを見せられてもよくわからない可能性があるので、デフォルトの`StatefulWidget`を書き換えながら説明していく。

### まずはデフォルトのコードをクラス毎に切り分ける

↓これは、`flutter create`しただけのデフォルトの`lib/main.dart`
コメント部分を全削除したが、それ以外はそのまま。

```lib/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),
      ),
    );
  }
}
```
このまま、進めてもいいが、widget毎にファイルを分けていきたいので、ここでも`MyAppクラス`と`MyHomePageクラス`を別ファイルにわけることから始める。

`lib/home.dart`を作成し、以下のように`main.dart`の中身を移行する。

```lib/home.dart
import 'package:flutter/material.dart';

//// ここから
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),
      ),
    );
  }
}
//// ここまで、main.dartから切り取り→ペースト
```

そして、`main.dart`はというと、

```lib/main.dart
import 'package:flutter/material.dart';
import 'home.dart';   // home.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'),
    );
  }
}

// 残りの部分はhome.dartに移行
```
`import 'home.dart`を追加したが、`MyHomePageクラス`を移行したことで、だいぶスッキリした。

この時点で一度ビルドし、動作を確認してみると、正常に動作している。
![Simulator Screen Shot - iPhone SE (2nd generation) - 2020-10-17 at 14.48.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/529880/e2e9a813-1ecd-92ae-3a13-79b33b76ca82.png)

### StatefulWidgetの内容を再確認

さて、これから`MyHomePageクラス`を`Stateless`に書き換えていくのだが、その前に、既存の`Stateful`のコードの内容をおさらいしておく。　

```lib/home.dart
import 'package:flutter/material.dart';

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),
      ),
    );
  }
}
```
コードは先ほど移行した状態なので、↑の通り。
`FloatingActionButton`を押すことで、`_incrementCounterメソッド`が呼び出される。
イメージ図↓
![Simulator Screen Shot - iPhone SE (2nd generation) - 2020-10-17 at 14.48.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/529880/7b40d0eb-ef6e-2765-f172-45ada404fa46.png)


```dart

void _incrementCounter() {
    setState(() {
      _counter++;
    });
}
```
`_incrementCounterメソッド`の処理は↑の内容だが、`setState((){});`で`_counter++;`を囲んでいることに注目してほしい。

これが仮に、

```dart

void _incrementCounter() {
  _counter++;
}
```
だった場合、試してもらえればわかると思うが、画面上の数字は増えない。

では、

```dart

void _incrementCounter() {
  _counter++;
  print(_counter);
}
```
としたらどうか？
画面の数字は増えないが、コンソールにはボタンを押すたびに1ずつ加算された数字が表示されるはずだ。

つまり、内部的に値は増えているが、それが画面には反映されていないのである。
ここで、`setState((){});`を使うと、処理が実行された際に画面を再描画するため、値が反映された状態で表示される。

イメージ図↓
![setstate.001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/529880/d8192c17-a7e4-5523-d31f-2ad2d85419ca.png)

`setState((){});`は`StatefulWidget`では使えるが、`StatelessWidget`では使えない。
そのため、`StatelessWidget`では別の方法で、値が変更された際に画面を再描画する必要があるのだが、そのために使うのが、`Provider`である。

### Providerを導入する
さて、Providerの役割がわかったところで、実際にProviderを導入していく。

こちらのパッケージがメジャーなので、インストールする。
https://pub.dev/packages/provider/install

インストールできたら、`lib/home.dart`に`import 'package:provider/provider.dart';`を追記する。
これで、Providerを使う準備は完了。

先に完成したコードを載せる。

```lib/home.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';  // 変更箇所 Providerを使うためのimport

// 変更箇所
class CounterStore with ChangeNotifier {
  int _counter = 0;

  void _incrementCounter() {   
    _counter++;
    notifyListeners();
  }
}

class MyHomePage extends StatelessWidget {     // 変更箇所
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  Widget build(BuildContext context) {
   // 変更箇所
    return ChangeNotifierProvider(
        create: (context) => CounterStore(),
        child: Builder(builder: (BuildContext context) {
          final store = Provider.of<CounterStore>(context);
          final _counter = store._counter;
          return Scaffold(
            appBar: AppBar(
              title: Text(title),  // 変更箇所 widget.title → 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: store._incrementCounter,  // 変更箇所
              tooltip: 'Increment',
              child: Icon(Icons.add),
            ),
          );
        }));
  }
}
```

順番に説明していく。
まずは、この部分。

```lib/home.dart

class CounterStore with ChangeNotifier {
  int _counter = 0;

  void _incrementCounter() {
    _counter++;
    notifyListeners();
  }
}
```
`MyHomePageクラス`の中で定義していた、`_counter`と`_incrementCounter()`を、新たに作成した`CounterStore`クラスに移行。

`class CounterStore with ChangeNotifier`と書くのがポイント。
そしてもう一つのポイントが、

```lib/home.dart
void _incrementCounter() {
    _counter++;
    notifyListeners();
  }
```
というように、加算処理の後に、`notifyListeners();`をつけること。
`with ChangeNotifier`をつけたクラスの中で、`notifyListeners();`を使うことで、値の変更を検知することができる。

`SetState`と同じような役割だとイメージしてもらえればOK。



次は`MyHomePageクラス`の中身を見ていく。

まず気づく違いは、`Stateful`→`Stateless`になっている点。
それに伴い、`widget.title`→`title`と引数を使用する書き方が変わっている。

そして重要なのが、以下の部分。

```lib/home.dart
return ChangeNotifierProvider(
        create: (context) => CounterStore(),
        child: Builder(builder: (BuildContext context) {
          final store = Provider.of<CounterStore>(context);
          final _counter = store._counter;
          return Scaffold(
       ////  以下省略  
```

`Scaffold`を`ChangeNotifierProvider`でラップしている。

```lib/home.dart
return ChangeNotifierProvider(
        create: (context) => CounterStore(),
        child: Builder(builder: (BuildContext context) {
```
この部分は、Providerを使うための決まり文句だと思ってもらえればいい。

厳密には、`ChangeNotifierProvider`以外にも色々とあり、`Provider`=`ChangeNotifierProvider`ではないのだが、個人的に`ChangeNotifierProvider`が使いやすいので、本記事ではそれについて書いている。

話を戻す。
`create: (context) => CounterStore(),`の部分は、先ほど説明した`class CounterStore with ChangeNotifier`を使うための記述。

続く、`final store = Provider.of<CounterStore>(context);`で`CounterStore`のインスタンスを作成し、`final _counter = store._counter;`で変数を取得している。

それ以外は、ほぼコードの違いはないが、もう一点↓の部分

```lib/home.dart
 floatingActionButton: FloatingActionButton(
       onPressed: store._incrementCounter,   // ここ
       tooltip: 'Increment',
       child: Icon(Icons.add),
  ),
```

先ほど作成した、`CounterStore`のインスタンスである`store`のメソッドを使用している。

さらに、`CounterStoreクラス`部分は、別ファイルに切り分けることができる。

`lib/counter_store.dart`を作成し、分けると以下のようになる。

```lib/counter_store.dart

import 'package:flutter/material.dart';

// 他ファイルから使用するために、変数とメソッドの_を削除。
class CounterStore with ChangeNotifier {
  int counter = 0;   // _counter→counter

  void incrementCounter() {   // _incrementCounter→incrementCounter
    counter++;
    notifyListeners();
  }
}
```

```lib/home.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'counter_store.dart';   // 追加

class MyHomePage extends StatelessWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
        create: (context) => CounterStore(),
        child: Builder(builder: (BuildContext context) {
          final store = Provider.of<CounterStore>(context);
          final _counter = store.counter;
          return Scaffold(
            appBar: AppBar(
              title: Text(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: store.incrementCounter,
              tooltip: 'Increment',
              child: Icon(Icons.add),
            ),
          );
        }));
  }
}
```

ここまでの手順をまとめると、以下の通り。
Providerを使って、`StatefulWidget`→`StatelessWidget`に書き換える
ロジック部分=`CounterStoreクラス`を別ファイルに切り分ける

この時点で、見た目=ビューとロジックを分けることができ、コードが見やすくなっている。

## 複数Widgetを組み合わせた画面を作る

Providerの書き方を説明したところで、もう少しだけ応用的な使い方を紹介する。


![Simulator Screen Shot - iPhone SE (2nd generation) - 2020-10-17 at 17.51.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/529880/b0e8e494-8b25-f51c-2f02-a0effd765fb4.png)

↑こんな画面を作る。
画面自体は非常にシンプルで、`FloatingActionButton`を`RaisedButton`に変えただけ。
(ついでに、テキストも削除した)

コードは以下の通り。

```lib/home.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'counter_store.dart';

class MyHomePage extends StatelessWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
        create: (context) => CounterStore(),
        child: Builder(builder: (BuildContext context) {
          final store = Provider.of<CounterStore>(context);
          final _counter = store.counter;
          return Scaffold(
            appBar: AppBar(
              title: Text(title),
            ),
            body: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  Text(                   // 表示
                    '$_counter',
                    style: Theme.of(context).textTheme.headline4,
                  ),
                  RaisedButton(          // ボタン表示
                      onPressed: store.incrementCounter,
                      child: Text('Increment'))
                ],
              ),
            ),
          );
        }));
  }
}
```

`Column`の子ウィジットとして、`Text`と`RaisedButton`があるというシンプルな構成。


ここで、`Text`と`RaisedButton`を別ファイルに切り出してみる。

それぞれ、`lib/counter_num.dart`と`lib/counter_button.dart`を作成し、以下のように書く。

```lib/counter_num.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'counter_store.dart';

class CounterNum extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final store = Provider.of<CounterStore>(context);
    final _counter = store.counter;
    return Text(
      '$_counter',
      style: Theme.of(context).textTheme.headline4,
    );
  }
}
```

```lib/counter_button.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'counter_store.dart';

class CounterButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final store = Provider.of<CounterStore>(context);
    return RaisedButton(
        onPressed: store.incrementCounter, child: Text('Increment'));
  }
}
```

それぞれ`StatelessWidget`としてクラスを定義し、`Text`、`RaisedButton`を返している。

そして、こうして別ファイルに定義することで、`lib/home.dart`は以下のようになる。

```lib/home.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'counter_store.dart';
import 'counter_num.dart';      // importを追記
import 'counter_button.dart';   // importを追記

class MyHomePage extends StatelessWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
        create: (context) => CounterStore(),
        child: Builder(builder: (BuildContext context) {
          return Scaffold(
            appBar: AppBar(
              title: Text(title),
            ),
            body: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                   CounterNum(), 
                   CounterButton(),
                ],
              ),
            ),
          );
        }));
  }
}
```

Columnの中身が、先ほど切り分けたクラスに変わっただけだが、幾分かスッキリしている。

この例だと、`Text`と`RaisedButton`のため、分けたところでそんなに変わりはないが、内容が複雑になるほど、こうして切り分けて書くことで、見やすいコードになる。

そして、Widgetを分けて書くためには、Providerを使ってロジック部分を切り分け、

```dart
final store = Provider.of<CounterStore>(context);

final _counter = store.counter;   // 変数
store.incrementCounter();         // メソッド
```
というように、使用する箇所で呼び出す必要がある。

## まとめ
この記事の内容をまとめると、
**Providerを使うことで、ビューとロジックの切り分け、Widgetを細かく切り分けて書くことができる。**
そしてそれにより、**Widgetのネストが深くなるような複雑なコードでも、比較的見やすく、管理しやすくなる。**

冒頭で書いた、**「Widgetを分けて書くことができる」**とは**どういうことで、それの何がいいのか？**の答えである。

繰り返しになるが、本記事で取り上げた例では、いまいちその効果が理解できないかもしれない。
ただ、そんなに使うのが難しいわけでもないため、Providerを使ってみて、開発を進める中で、そのメリットを体感してもらえればと思う。

僕自身、そもそも`StatelessWidget`と`StatefulWidget`の違いもわからない状態から、個人開発をする中で、一応は`Provider`を使えるようになったので、同じようにFlutter初心者のお役に立てれば嬉しい。