Flutterの記事を整理し本にしました
- 本稿の記事を含む様々な記事を体系的に整理し本にまとめました
- 今後はこちらを最新化するため、最新情報はこちらをご確認ください
- 10万文字を超える超大作になっています(笑)
はじめに
- いままで何となくにしていたInheritedWidgetを学びなおしたので、整理して記事にしてみました。
まとめ
ProviderとChangeNotifier
本チャプターは、下記のGoogle I/O'19の情報をベースとしています。
InheritedWidget
を使うと上位のWidgetの値を使えますが、InheritedWidget
を直接使うことは少なく、Provider
という便利なパッケージがあるので、こちらを使うことが多いです。
InheritedWidgetについて、よくわからないという方は、InheritedWidgetをご参照ください
Providerは、InheritedWidgetラッパーライブラリです。
使う場合は、ライブラリの読み込みが必要です。
dependencies:
provider: "^5.0.0"
使い方はInheritedWidget
に似ており、InheritedWidget
の代わりに、使うWidgetの上位にProvider
を挟んでおき、下位Widgetでその値を使います。
前回のInheritedWidgetとの対比で確認してみます。
import 'package:flutter/material.dart';
import 'package:hello_world/Widgets.dart';
import 'package:provider/provider.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
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++;
});
print("count:" + _counter.toString());
}
// Scaffoldの下のCenter部分を先に静的に作っておき、作り返さないように制御
// 深い階層の伝播は証明できたためにシンプルにCenter->WidgetAに変更
final Widget _widget = Center(child: WidgetA());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title!),
),
- //body: MyInheritedWidget(count: _counter, child: _widget),
+ body: Provider<int>.value(value: _counter, child: _widget),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class WidgetA extends StatelessWidget {
@override
Widget build(BuildContext context) {
int count;
try {
- //MyInheritedWidget _myInheritedWidget = MyInheritedWidget.of(context);
- //count = _myInheritedWidget.count;
+ count = Provider.of<int>(context);
} catch (e) {
count = 0;
}
return Text("$count", style: TextStyle(fontSize: 100));
}
}
ほぼInheritedWidget
がProvider
に置き換わっただけであることが確認できます。
Consumer
前回のようにWidgetAを自作せずに、Provider
パッケージが提供するConsumer
を使うとより簡単に同じ機能を実現できます。
// _widgetの定義以外は同じのため省略
- final Widget _widget = Center(child: WidgetA());
+ final Widget _widget = Center(
+ child: Consumer<int>(
+ builder: (context, value, _) => Text(
+ value.toString(),
+ style: TextStyle(fontSize: 100),
+ ),
));
もちろん、再構築される範囲はこのConsumerの範囲のみです。
Providerにはいくつかの種類が提供されています。
この後紹介する変更を検知して動作するChangeNotifierProvider
や複数のProvider
を取り扱えるmultiProvider
などがあります
ChangeNotifierとChangeNotifierProvider
Provider
の上位Widgetの値の取得を発展させ、データが変わったことを通知する(notifyListeners
)とデータが変わったことを知りWidgetを作り直す(ChangeNotifierProvider
)を使って、状態管理を行うことができます。
以下のようなスライダーとその値を表示させるシンプルなアプリで動作を確認します。
import 'package:flutter/foundation.dart';
class MyData with ChangeNotifier {
double _value = 0.5;
// getter
double get value => _value;
// setter
set value(double value) {
_value = value;
notifyListeners(); //通知
}
}
まず、データを保持し、変更されたら通知するクラスを作ります。
ChangeNotifier
をwith
でmixin
することで、通知を行うnotifyListeners
が使えるようになります。
この関数が呼ばれると、変更を監視しているWidget
に変更が通知されます。
import 'package:flutter/material.dart';
import 'package:hello_world/mydata.dart';
import 'package:provider/provider.dart';
class MySlider extends StatefulWidget {
@override
createState() => _MySliderState();
}
class _MySliderState extends State<MySlider> {
@override
Widget build(BuildContext context) {
final mydata = Provider.of<MyData>(context);
return Slider(
value: mydata.value, onChanged: (value) => mydata.value = value);
}
}
次にスライダーです。
スライダーで設定した値をMyDataに設定します。
また、スライダの位置をvalueで設定するため、入力値としてMyDataの値の取得も行います。
void main() { /* 変更なしのため省略 */}
class MyApp extends StatelessWidget { /* 変更なしのため省略 */}
class MyHomePage extends StatefulWidget { /* 変更なしのため省略 */}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (BuildContext context) => MyData(),
child: Scaffold(
appBar: AppBar(
title: Text(widget.title!),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Consumer<MyData>(
builder: (context, mydata, _) => Text(
mydata.value.toStringAsFixed(2),
style: TextStyle(fontSize: 100),
),
),
MySlider(),
],
)),
);
}
}
ChangeNotifierProviderを上位に挟み、変更された時にその変更を検知できるようになっています。
処理の流れは下記のとおりです。
- SliderはProvider.ofでMyDataにアクセスし、値の取得や設定を行う
- MyDataのsetterの中で、notifyListenersでリスナーに変更を通知する
- Consumerが変更を検知して、Textへの値の設定と作り直しを行う
context.select/context.read
下記は、BuildContextのextensionとして、コードをシンプルにするために役立ちます。
- context.select(変更を監視する)
- context.read(変更を監視しない)
import 'package:flutter/material.dart';
import 'package:hello_world/mydata.dart';
import 'package:provider/provider.dart';
class MySlider extends StatefulWidget {
@override
createState() => _MySliderState();
}
class _MySliderState extends State<MySlider> {
@override
Widget build(BuildContext context) {
// context.select,readを使ってアクセス
return Slider(
value: context.select((MyData mydata) => mydata.value),
onChanged: (value) => context.read<MyData>().value = value);
}
}
void main() { /* 変更なしのため省略 */}
class MyApp extends StatelessWidget { /* 変更なしのため省略 */}
class MyHomePage extends StatefulWidget { /* 変更なしのため省略 */}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (BuildContext context) => MyData(),
child: Scaffold(
appBar: AppBar(
title: Text(widget.title!),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Consumer<MyData>(
// context.readを使ってアクセス
builder: (context, schedule, _) => Text(
context.select(
(MyData mydata) => mydata.value.toStringAsFixed(2)),
style: TextStyle(fontSize: 100))),
MySlider()
],
)),
);
}
}
Providerパッケージをより改善したRiverpodパッケージもありますが、今回は取り扱いません。
「グローバルでの定義が可能」「ランタイムエラーを起こさない」「同じ型での複数探索が可能」など改良が加えられています。
Providerデザインパターン
本チャプターで見てきた内容はProviderデザインパターンと呼ばれるものです。
開発の中級4:StreamとBLoCデザインパターンでは、Streamというものを用いて、入力、ビジネスロジック、出力の3つを分離するデザイパターンを紹介しました。
こちらは、Streamを使ってビジネスロジックを分離する考え方でしたが、Providerデザインパターンは、データの変更をnotifyListnersで通知し、ChangeNotifierProviderでその変更を検知することで、入出力とビジネスロジックを分離します。
整理すると、下記のようになります。
上記の整理は基本的な考え方に基づくもので、現実的には組み合わせたりプロジェクトの案件に合わせてカスタマイズされることが多くあります。