前回記事では、StatefulWidgetを使用して異なる画面から受け取った値で状態更新を行い、画面の再描画(リビルド)を実現してみました。
今回は、Providerを使用してそれを実現してみたいと思います。
Providerとは
Providerとは、「状態管理」に使われるパッケージのこと。
ざっくりと説明しますが知っている方は飛ばしてください。
状態管理について
Flutterには、データに変更があった際に画面を再描画する仕組みがあります。
また、そのデータのことを「状態」と呼んだりします。
これまでは、StatefulWidget
を使えば画面の再描画(リビルド)が実現できておりました。
しかし、StatefulWidget
はそのクラス内でUIを構築する処理と状態管理する処理を記述する必要があります。
したがって、各画面で状態を共有したい場合には向いていません。
Providerの登場
そこで、Providerの登場です。
Providerを使えば状態管理する処理を分けれるので、状態の共有がシンプルに実現できます。
GoogleもProviderパッケージの使用を推奨しており、今のところ状態管理を行うにはproviderを第一本線ととらえて問題ないでしょう。
Providerのメリット
- 再描画する範囲を限定的にできる
→表示速度の向上が期待できる - UIを構築する処理と状態を更新する処理を分けれる
- ページ間のWidgetでも状態を簡単に共有できる
- わりと複雑なWidget構成にも対応できる
今回の例では、Widgetの数も少ないですし、ほとんどのWidgetをConsumer
の配下にしています。
必要な個所のみレンダリング対象としたケースはこちらを参考ください。
Providerイメージ
図は正確でないかもですが、イメージはこんな感じ。
まずは、状態の変更と通知を行うクラスを準備。
(1)そのクラスのインスタンスより状態を更新したら、
(2)notifyListeners()
によりProviderに変更を通知。
(3)再描画対象のWidgetのみリビルドされて、画面が更新されます。
provider実装手順
それでは、前回記事をProviderを使って実現してみましょう。
実装手順はこちら。
-
Provider
パッケージをインストール -
MaterialApp
の上にChangeNotifierProvider
を配置 -
changeNotifier
クラスを拡張したクラスを作成 - 状態を更新したいところで
notifyListeners()
を実行 - 再描画対象としたいWidgetを
Consumer
でラップする
順に説明します。
1.Providerパッケージをインストール
pubspec.yaml
に以下を追加して、$ flutter pub get
を実行。
dependencies:
provider: ^4.3.2+1
バージョンはこちらで確認ください。
https://pub.dev/packages/provider
2.MaterialAppの上にChangeNotifierProviderを配置
Providerとして登録するクラスを、ChangeNotifierProvider
というのを使って以下のような形で登録します。
MultiProvider(
providers: [
ChangeNotifierProvider<プロバイダー名>(
create: (context) => プロバイダー名(),
),
],
),
Providerの数が複数ある場合は、Providers:[]
内に追加していきましょう。
今回はResultProvider
というクラスをProviderとして登録します。
コードは以下のとおり。
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider<ResultProvider>(
create: (context) => ResultProvider(),
),
],
child: MaterialApp(
home: MyHomePage(),
),
),
);
}
ChangeNotifierProvider
の配置箇所は、状態更新の通知を受け取りたいWidgetより親の階層であれば大丈夫です。
ただ、上図コードで示したようにrunApp
のど頭(MaterialApp
の上)で宣言しておけば、後で使用箇所を変更したいときなどは修正不要なので楽ですね。
3. changeNotifierクラスを拡張したクラスを作成
changeNotifier
クラスを拡張したResultProvider
クラスを作成します。
class ResultProvider extends ChangeNotifier {
・・・
}
4. 状態を更新したいところでnotifyListeners()を実行
notifyListeners()
を実行すると、Providerを介して、次手順で示すConsumer
配下のWidgetがリビルドされます。
class ResultProvider extends ChangeNotifier {
String _result;
ResultProvider() {
initValue();
}
// 初期化
void initValue() {
this._result = "遷移先に移動";
}
void refresh() {
initValue();
notifyListeners(); // Providerを介してConsumer配下のWidgetがリビルドされる
}
void updateText(String str) {
_result = str;
}
void notify() {
notifyListeners(); // Providerを介してConsumer配下のWidgetがリビルドされる
}
}
5. 再描画対象としたいWidgetをConsumerでラップする
Consumer
を配置すると、Consumer
配下のWidgetがリビルド対象となります。
Consumer<ResultProvider>(builder: (context, model, child) {
// changeNotifier()実行で、この中のWidgetが再描画される
}
今回はappBar以外の遷移元ページや遷移先ページで使用しています。
return Scaffold(
appBar: appBar(),
body: Consumer<ResultProvider>(builder: (context, model, _) {
return _renderText(model);
}),
floatingActionButton:
Consumer<ResultProvider>(builder: (context, model, _) {
return FloatingActionButton(
onPressed: () {
model.refresh(); // ResultProviderのメソッド
},
child: Icon(
Icons.refresh,
),
);
}),
);
builder
の第二引数model
は、ChangeNotifierクラスを拡張したProviderクラスのインスタンスとなります。
このmodel
を使用して、Provideクラスのメソッドやプロパティ(状態)の参照を行います。
なお、model
は自由に名称変更可能です。
小分けにした関数_renderText()
内はこんな感じ。
Widget _renderText(ResultProvider model) {
// print('text:${model._result}');
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
model._result, // ResultProviderのプロパティ
style: Theme.of(context).textTheme.headline5,
),
RaisedButton(
child: Text('Go to Edit Page'),
onPressed: () async {
model.updateText('Hello! from HomePage.');
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
// 引数に遷移元から遷移先へ渡す値を設定
EditPage(),
),
);
// print(result);
model.notify(); // ResultProviderのメソッド
},
),
],
),
);
}
##サンプルコード全文
今回のサンプルコード全文です。
前回記事のStatefulWidgetでは、Navigatorで値を渡しました。
今回はProviderのインスタンスmodel
で状態を更新したり、受け取ったりしています。
ちょっと無理やりProviderを使った感がありますが、一例と考えていただければ幸いです。
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:provider/provider.dart';
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider<ResultProvider>(
create: (context) => ResultProvider(),
),
],
child: MaterialApp(
home: MyHomePage(),
),
),
);
}
class ResultProvider extends ChangeNotifier {
String _result;
ResultProvider() {
initValue();
}
// 初期化
void initValue() {
this._result = "遷移先に移動";
}
void refresh() {
initValue();
notifyListeners(); // Providerを介してConsumer配下のWidgetがリビルドされる
}
void updateText(String str) {
_result = str;
}
void notify() {
notifyListeners(); // Providerを介してConsumer配下のWidgetがリビルドされる
}
}
// 遷移元ページ
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// テキスト表示
Widget _renderText(ResultProvider model) {
// print('text:${model._result}');
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
model._result, // ResultProviderのプロパティ
style: Theme.of(context).textTheme.headline5,
),
RaisedButton(
child: Text('Go to Edit Page'),
onPressed: () async {
model.updateText('Hello! from HomePage.');
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
// 引数に遷移元から遷移先へ渡す値を設定
EditPage(),
),
);
// print(result);
model.notify(); // ResultProviderのメソッド
},
),
],
),
);
}
// サンプル1:Scaffold全体をリビルド
// return Consumer<ResultProvider>(builder: (context, model, _) {
// return Scaffold(
// appBar: appBar(),
// body: _renderText(model),
// floatingActionButton: FloatingActionButton(
// onPressed: () {
// model.refresh();
// },
// child: Icon(
// Icons.refresh,
// ),
// ),
// );
// });
// サンプル2:appBar以外をリビルド
return Scaffold(
appBar: appBar(),
body: Consumer<ResultProvider>(builder: (context, model, _) {
return _renderText(model);
}),
floatingActionButton:
Consumer<ResultProvider>(builder: (context, model, _) {
return FloatingActionButton(
onPressed: () {
model.refresh(); // ResultProviderのメソッド
},
child: Icon(
Icons.refresh,
),
);
}),
);
}
appBar() {
print('appBar実行');
return AppBar(
title: Text('My Home Page(遷移元)'),
);
}
}
// 遷移先ページ
class EditPage extends StatelessWidget {
final receive;
const EditPage({Key key, this.receive}) : super(key: key);
@override
Widget build(BuildContext context) {
return Consumer<ResultProvider>(
builder: (context, model, child) {
return WillPopScope(
onWillPop: () {
model.updateText('Thank you! from 戻るアイコン'); // ResultProviderのメソッド
Navigator.pop(context);
return Future.value(false);
},
child: Scaffold(
appBar: AppBar(
title: Text('Edit Page(遷移先)'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
model._result,
style: Theme.of(context).textTheme.headline5,
),
RaisedButton(
child: Text('Return'),
onPressed: () {
model.updateText(
'Thank you! from 戻るボタン'); // ResultProviderのメソッド
Navigator.pop(context);
}),
],
),
),
),
);
},
);
}
}
notifyListeners()
を実行した際、Consumer配下のみ再描画されることを確認するために、
コード内のサンプル1とサンプル2を切り替えて実行してみてください。
サンプル1では、print('appBar実行');
が実行、出力されますが、サンプル2ではそうならないはずです。
参考
こちらでもうひとつProviderの使用例を示しています。