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
コメント部分を全削除したが、それ以外はそのまま。
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
の中身を移行する。
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
はというと、
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クラス
を移行したことで、だいぶスッキリした。
この時点で一度ビルドし、動作を確認してみると、正常に動作している。
StatefulWidgetの内容を再確認
さて、これからMyHomePageクラス
をStateless
に書き換えていくのだが、その前に、既存のStateful
のコードの内容をおさらいしておく。
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メソッド
が呼び出される。
イメージ図↓
void _incrementCounter() {
setState(() {
_counter++;
});
}
_incrementCounterメソッド
の処理は↑の内容だが、setState((){});
で_counter++;
を囲んでいることに注目してほしい。
これが仮に、
void _incrementCounter() {
_counter++;
}
だった場合、試してもらえればわかると思うが、画面上の数字は増えない。
では、
void _incrementCounter() {
_counter++;
print(_counter);
}
としたらどうか?
画面の数字は増えないが、コンソールにはボタンを押すたびに1ずつ加算された数字が表示されるはずだ。
つまり、内部的に値は増えているが、それが画面には反映されていないのである。
ここで、setState((){});
を使うと、処理が実行された際に画面を再描画するため、値が反映された状態で表示される。
setState((){});
はStatefulWidget
では使えるが、StatelessWidget
では使えない。
そのため、StatelessWidget
では別の方法で、値が変更された際に画面を再描画する必要があるのだが、そのために使うのが、Provider
である。
Providerを導入する
さて、Providerの役割がわかったところで、実際にProviderを導入していく。
こちらのパッケージがメジャーなので、インストールする。
https://pub.dev/packages/provider/install
インストールできたら、lib/home.dart
にimport 'package:provider/provider.dart';
を追記する。
これで、Providerを使う準備は完了。
先に完成したコードを載せる。
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),
),
);
}));
}
}
順番に説明していく。
まずは、この部分。
class CounterStore with ChangeNotifier {
int _counter = 0;
void _incrementCounter() {
_counter++;
notifyListeners();
}
}
MyHomePageクラス
の中で定義していた、_counter
と_incrementCounter()
を、新たに作成したCounterStore
クラスに移行。
class CounterStore with ChangeNotifier
と書くのがポイント。
そしてもう一つのポイントが、
void _incrementCounter() {
_counter++;
notifyListeners();
}
というように、加算処理の後に、notifyListeners();
をつけること。
with ChangeNotifier
をつけたクラスの中で、notifyListeners();
を使うことで、値の変更を検知することができる。
SetState
と同じような役割だとイメージしてもらえればOK。
次はMyHomePageクラス
の中身を見ていく。
まず気づく違いは、Stateful
→Stateless
になっている点。
それに伴い、widget.title
→title
と引数を使用する書き方が変わっている。
そして重要なのが、以下の部分。
return ChangeNotifierProvider(
create: (context) => CounterStore(),
child: Builder(builder: (BuildContext context) {
final store = Provider.of<CounterStore>(context);
final _counter = store._counter;
return Scaffold(
//// 以下省略
Scaffold
をChangeNotifierProvider
でラップしている。
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;
で変数を取得している。
それ以外は、ほぼコードの違いはないが、もう一点↓の部分
floatingActionButton: FloatingActionButton(
onPressed: store._incrementCounter, // ここ
tooltip: 'Increment',
child: Icon(Icons.add),
),
先ほど作成した、CounterStore
のインスタンスであるstore
のメソッドを使用している。
さらに、CounterStoreクラス
部分は、別ファイルに切り分けることができる。
lib/counter_store.dart
を作成し、分けると以下のようになる。
import 'package:flutter/material.dart';
// 他ファイルから使用するために、変数とメソッドの_を削除。
class CounterStore with ChangeNotifier {
int counter = 0; // _counter→counter
void incrementCounter() { // _incrementCounter→incrementCounter
counter++;
notifyListeners();
}
}
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の書き方を説明したところで、もう少しだけ応用的な使い方を紹介する。
↑こんな画面を作る。
画面自体は非常にシンプルで、FloatingActionButton
をRaisedButton
に変えただけ。
(ついでに、テキストも削除した)
コードは以下の通り。
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
を作成し、以下のように書く。
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,
);
}
}
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
は以下のようになる。
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を使ってロジック部分を切り分け、
final store = Provider.of<CounterStore>(context);
final _counter = store.counter; // 変数
store.incrementCounter(); // メソッド
というように、使用する箇所で呼び出す必要がある。
まとめ
この記事の内容をまとめると、
Providerを使うことで、ビューとロジックの切り分け、Widgetを細かく切り分けて書くことができる。
そしてそれにより、Widgetのネストが深くなるような複雑なコードでも、比較的見やすく、管理しやすくなる。
冒頭で書いた、**「Widgetを分けて書くことができる」とはどういうことで、それの何がいいのか?**の答えである。
繰り返しになるが、本記事で取り上げた例では、いまいちその効果が理解できないかもしれない。
ただ、そんなに使うのが難しいわけでもないため、Providerを使ってみて、開発を進める中で、そのメリットを体感してもらえればと思う。
僕自身、そもそもStatelessWidget
とStatefulWidget
の違いもわからない状態から、個人開発をする中で、一応はProvider
を使えるようになったので、同じようにFlutter初心者のお役に立てれば嬉しい。