(2021-01-05 追記)
この記事は筆者の理解が浅い段階で書かれたものです!
後にちゃんと書いた記事がありますので、その記事をご覧ください。
InheritedWidgetの目的と使い方【Flutter】
この記事は近日中に非公開にする予定です。
はじめに
InheritedWidgetの使い方のほんの一端がやっっとわかったので説明しますが、
まだまだ全然理解不足の状態で書いている記事なので、全然違うことを言っている可能性があります!
それでも良いという方はお読みください。
「インスタンス」という言葉を多用するので、「クラス」と「インスタンス」の関係がよくわからない人はまずそこを調べてきてくださいね!
例として使うアプリの概要
以下の用にWidgetを3つ並べたものを作ります。
Column(children: [
WidgetA(),
WidgetB(),
WidgetC(),
]),
-
WidgetA変数xを+1するボタン -
WidgetB他の動作と何も関係しない、単なる固定テキスト -
WidgetC変数xの値を表示するテキスト
目標
WidgetAのボタンを押した時に
- 内容に変更がある
WidgetCは rebuild される - 内容に変更のない
WidgetAとWidgetBは rebuild されない
という状態を目指します。
というのも、Widgetの rebuild は計算コストがかかるので、内容に変更がないなら rebuild しない方がいいわけです。
とりあえず書くとこうなる
僕がInheritedWidgetを勉強する前にしていた書き方です。
各build()関数の中でprint();を呼んで、どれが rebuild されたのかわかるようにしてあります。
Widgetが rebuild されるとbuild()関数が呼ばれるので、print();によりコンソールに出力が出て、それでわかるわけですね。
WidgetAのボタンが押された時に起こることを番号付きでコメントしてありますので追ってみてください。
コード
import 'package:flutter/material.dart';
void main() => runApp(MyStatefulWidget());
class MyStatefulWidget extends StatefulWidget {
@override
State createState() => _MyState();
}
class _MyState extends State<MyStatefulWidget> {
int _x = 0;
int get x => _x;
void increment() {
setState(() {// 2. setState()により、Stateがrebuildされる
_x++;
});
}
static _MyState of(BuildContext context) {
return context.ancestorStateOfType(TypeMatcher<_MyState>());
}
@override
Widget build(BuildContext context) {// 3. Stateのrebuildのため、build()が呼ばれる
print("_MyState.build() is called!! x:$x");
return MaterialApp(
home: Scaffold(
appBar: AppBar(),
body: Column(children: [
WidgetA(),// 4. WidgetA,B,Cの新しいインスタンスが作られる
WidgetB(),
WidgetC(),
]),
),
);
}
}
class WidgetA extends StatelessWidget {
@override
Widget build(BuildContext context) {
print("AAA WidgetA.build() is called!!!!");
return RaisedButton(
child: Text("WidgetA is the incrementer"),
onPressed: () {
_MyState.of(context).increment();// 1. ボタンが押されたら、_MyStateのincrement()を呼ぶ
},
);
}
}
class WidgetB extends StatelessWidget {
@override
Widget build(BuildContext context) {
print("BBB WidgetB.build() is called!!!!");
return Text("This is the constant WidgetB");
}
}
class WidgetC extends StatelessWidget {
@override
Widget build(BuildContext context) {
final int x = _MyState.of(context).x;
print("CCC WidgetC.build() is called!!!!");
return Text("Widget C x: $x");
}
}
毎回全てのWidgetが再構築(rebuild)されてしまう
上記のコードを実行して、WidgetAのボタンを押すと、画面のWidgetCの数値が更新されると同時に、
コンソールに下のような出力がでます。
flutter: _MyState.build() is called!! x:3
flutter: AAA WidgetA.build() is called!!!!
flutter: BBB WidgetB.build() is called!!!!
flutter: CCC WidgetC.build() is called!!!!
(xの値は押すたびに1ずつ増えていきます)
WidgetAのボタンを押す度に、WidgetA WidgetB WidgetC全てが rebuild されていますね。
よく考えると、ボタンを押す度に新しいインスタンスを生成しているのだから当然です。
(Dartではnewを省略できるので、コード内でWidgetA()と書いたらnew WidgetA()という意味です。つまり、新しいインスタンスの生成。)
これでは無駄が多いですね。WidgetAとWidgetBは使いまわしたい所です。
ちなみに
Widgetを rebuild しているからと言って、必ずしも全て再レンダリングされているされているわけではないらしいです。
このあたりのことは「エレメント」などを勉強するとわかるはずです。こちらが参考になると思います。でも内容は高度です。
なお、再レンダリングされていなくても、Widgetの rebuild 自体にコストがかかっているので、無駄は無駄。
先にWidgetのインスタンスを作って渡してしまう
ボタンを押す度に毎回WidgetA WidgetB WidgetCのインスタンスを生成するのはやめましょう。
最初からMyStatefulWidgetのコンストラクタに一通りのWidgetツリーのインスタンスを渡してしまって、そのまま保持してもらいましょう。
Stateの中でwidgetと記述すると、その親にあたるStatefulWidgetが取得できることに注意して、コメントの所を見てください。
コード
import 'package:flutter/material.dart';
void main() => runApp(MyStatefulWidget(
child: MaterialApp(
home: Scaffold(
appBar: AppBar(),
body: Column(children: [
WidgetA(), // ここで作ったインスタンスをずっと使う
WidgetB(),
WidgetC(),
]),
),
),
));
class MyStatefulWidget extends StatefulWidget {
MyStatefulWidget({Key key, this.child}) : super(key: key);
final Widget child; // ここに一通りの Widget ツリーを保持し続ける
@override
State createState() => _MyState();
}
class _MyState extends State<MyStatefulWidget> {
int _x = 0;
int get x => _x;
void increment() {
setState(() {
_x++;
});
}
static _MyState of(BuildContext context) {
return context.ancestorStateOfType(TypeMatcher<_MyState>());
}
@override
Widget build(BuildContext context) {
print("_MyAppState.build() is called!! x:$x");
return widget.child; // setState()によりbuild()が呼ばれると、保持してる Widget ツリーをそのまま返すだけ
}
}
class WidgetA extends StatelessWidget {
@override
Widget build(BuildContext context) {
print("AAA WidgetA.build() is called!!!!");
return RaisedButton(
child: Text("WidgetA is the incrementer"),
onPressed: () {
_MyState.of(context).increment();
},
);
}
}
class WidgetB extends StatelessWidget {
@override
Widget build(BuildContext context) {
print("BBB WidgetB.build() is called!!!!");
return Text("This is the constant WidgetB");
}
}
class WidgetC extends StatelessWidget {
@override
Widget build(BuildContext context) {
final int x = _MyState.of(context).x;
print("CCC WidgetC.build() is called!!!!");
return Text("Widget C x: $x");
}
}
更新されない!
これを実行してWidgetAボタンを押すと、画面の数値は更新されず、
コンソールには
flutter: _MyAppState.build() is called!! x:4
の行だけが出力されます。
WidgetA WidgetBが更新されないのはいいですが、WidgetCも更新されないのでは困ります。
最初に渡したインスタンスをそのまま何も変更せずに使いまわしてるのでこういうことになります。
これではさっきの方がマシですね。
InheritedWidgetを使おう!
ということで、これを解決するのがInheritedWidgetです。
まずInheritedWidgetを継承したクラスを作ります。名前は_MyInheritedWidgetにしてみます。
それを、Widgetツリー内のStateの直下に挟み込みます。
具体的には、_MyStateのbuild()関数が_MyInheritedWidget(...)をreturnするようにします。
そして、WidgetCが数値を参照する先を、_MyStateではなく、作成した_MyInheritedWidgetに変えます。
ポイント
このようにすると、_MyStateのsetState()が呼ばれる度にbuild()が呼ばれ、新しい_MyInheritedWidgetのインスタンスが返されることになります。
でも、保持してるWidgetA WidgetB WidgetCのインスタンスは、MyStatefulWidgetが保持してるインスタンスを再利用します。
-
InheritedWidget自体は rebuild される(毎回新しくインスタンスを作る) - 中身の
Widgetはもともとあったインスタンスを再利用する
というのがポイントですね。
コード
import 'package:flutter/material.dart';
void main() => runApp(MyStatefulWidget(
child: MaterialApp(
home: Scaffold(
appBar: AppBar(),
body: Column(children: [
WidgetA(),
WidgetB(),
WidgetC(),
]),
),
),
));
class MyStatefulWidget extends StatefulWidget {
MyStatefulWidget({Key key, this.child}) : super(key: key);
final Widget child;
@override
State createState() => _MyState();
}
// _MyInheritedWidget の定義
class _MyInheritedWidget extends InheritedWidget {
const _MyInheritedWidget({
Key key,
@required Widget child,
@required this.value,
}) : super(key: key, child: child);
final int value; // xの値を受け取って、InheritedWidget 内で保持するための枠
static _MyInheritedWidget of(BuildContext context) {
return context.inheritFromWidgetOfExactType(_MyInheritedWidget)
as _MyInheritedWidget;
}
// value値が変更された場合に、傘下のWidgetに更新を通知する。これがfalseの時は更新されない。
@override
bool updateShouldNotify(_MyInheritedWidget old) => old.value != value;
}
class _MyState extends State<MyStatefulWidget> {
int _x = 0;
int get x => _x;
void increment() {
setState(() {
_x++;
});
}
static _MyState of(BuildContext context) {
return context.ancestorStateOfType(TypeMatcher<_MyState>());
}
@override
Widget build(BuildContext context) {
print("_MyAppState.build() is called!! x:$x");
// _MyInheritedWidget のインスタンスを作り直して return する
return _MyInheritedWidget(
value: x, // _MyStateが保持しているxの値を、新しく作るInheritedWidgetインスタンスに渡す
child: widget.child, // MyStatefulWidgetが保持しているWidgetツリーを使いまわす
);
}
}
class WidgetA extends StatelessWidget {
@override
Widget build(BuildContext context) {
print("AAA WidgetA.build() is called!!!!");
return RaisedButton(
child: Text("WidgetA is the incrementer"),
onPressed: () {
_MyState.of(context).increment();
},
);
}
}
class WidgetB extends StatelessWidget {
@override
Widget build(BuildContext context) {
print("BBB WidgetB.build() is called!!!!");
return Text("This is the constant WidgetB");
}
}
class WidgetC extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 値の参照先を _MyState ではなく _MyInheritedWidget にする
final int x = _MyInheritedWidget.of(context).value;
print("CCC WidgetC.build() is called!!!!");
return Text("Widget C x: $x");
}
}
WidgetCだけが rebuild される!
これを実行して、WidgetAボタンを押すと、ちゃんと画面のWidgetCの数値が更新されます。
コンソールには下のような出力がでます。
flutter: _MyAppState.build() is called!! x:5
flutter: CCC WidgetC.build() is called!!!!
WidgetAとWidgetBは rebuild されていませんね!
これで目標が達成されました!やったね!
_MyInheritedWidgetを参照しているのはWidgetCだけなので、うまいことやってそれだけ更新してくれます。
(最後に)InheritedModelについて
この記事では紹介するに留めますが、InheritedModelというものもあります。
InheritedWidgetがxとyという複数の値を保持していて、
WidgetCがxを、WidgetDがyを参照してる時、
xだけが変化したら、WidgetCだけを更新して、WidgetDは更新したくないですよね。
そういう時はInheritedWidgetではなくInheritedModelを使うと良いです。
こちらの公式動画が、概要を映像でわかりやすく説明してくれます。見てみてください。
はい。ということで、終わります。 みなさんもFlutterやりましょうね!