(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やりましょうね!