LoginSignup
32
25

More than 3 years have passed since last update.

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

Posted at

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

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


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

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

これが仮に、


void _incrementCounter() {
  _counter++;
}

だった場合、試してもらえればわかると思うが、画面上の数字は増えない。

では、


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

としたらどうか?
画面の数字は増えないが、コンソールにはボタンを押すたびに1ずつ加算された数字が表示されるはずだ。

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

イメージ図↓
setstate.001.png

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

Providerを導入する

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

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

インストールできたら、lib/home.dartimport '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クラスの中身を見ていく。

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

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

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(
       ////  以下省略  

ScaffoldChangeNotifierProviderでラップしている。

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を使って、StatefulWidgetStatelessWidgetに書き換える
ロジック部分=CounterStoreクラスを別ファイルに切り分ける

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

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

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

Simulator Screen Shot - iPhone SE (2nd generation) - 2020-10-17 at 17.51.37.png

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

コードは以下の通り。

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の子ウィジットとして、TextRaisedButtonがあるというシンプルな構成。

ここで、TextRaisedButtonを別ファイルに切り出してみる。

それぞれ、lib/counter_num.dartlib/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としてクラスを定義し、TextRaisedButtonを返している。

そして、こうして別ファイルに定義することで、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の中身が、先ほど切り分けたクラスに変わっただけだが、幾分かスッキリしている。

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

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

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

final _counter = store.counter;   // 変数
store.incrementCounter();         // メソッド

というように、使用する箇所で呼び出す必要がある。

まとめ

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

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

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

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

32
25
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
32
25