Flutterの記事を整理し本にしました
- 本稿の記事を含む様々な記事を体系的に整理し本にまとめました
- 今後はこちらを最新化するため、最新情報はこちらをご確認ください
- 10万文字を超える超大作になっています(笑)
はじめに
- いままで何となくにしていたInheritedWidgetを学びなおしたので、整理して記事にしてみました。
まとめ
InheritedWidget
これまで、StatelessWidgetやStatefulWidgetが登場してきましたが、InheritedWidgetというものもあります。
Flutterでは、Widgetを入れ子構造で構築していくため、ツリーがどんどん深くなり、祖先や子孫をたどるのに時間がかかる傾向があります。
また、ツリーの根に近い部分から再構築する場合のコストも高くなります。
そこで、素早く祖先をたどったり、再構築を部分ツリーに限定することができれば処理効率が良くなります。
特徴1:O(1)でのWidgetやフィールドへのアクセス
Widgetが祖先のWidgetの変数にアクセスしたい場合、上位から順次パラメタ引数で渡していくのが一般的です
しかし、階層が深くなっていくと、すべてをパラメタで渡すのはプログラムが煩雑になり、素通しする中間のWidgetがでてきます。
また、仮にすべてのWidgetにグローバルキーを設定しておけば、このキーを使って直接アクセスができ、O(1)を実現できますが、全てにグローバルキーを付けるのも管理が大変になります。
そこで、祖先のWidgetやそのフィールドへ素早くアクセスできる手段があると効率的で、これを提供するのが、InheritedWidgetです。
InheritedWidgetは下位のツリーからO($1$)でアクセスできるようになっています。
Oはランダウ記号と呼ばれ、処理にかかる計算量の支配項を表すのに使われます。
O($1$) : 直接アクセス。ハッシュなど
O($n$) : データ数に比例。ループ処理の線型検索など
O($n^2$) : データの2乗に比例。二重ループ処理のバブルソートなど
O($log_2n$) : 要素を半分にしながら行う処理。二分探索など
O($nlog_2n$) : O($n$)とO($log_2n$)を組み合わせて行うような処理。ヒープソート。マージソートなど
今回もHelloWorldをサンプルに、動作を確認していきます。
import 'package:flutter/material.dart';
import 'package:hello_world/Widgets.dart';
import 'MyInheritedWidget.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++;
});
}
@override
Widget build(BuildContext context) {
// InheritedWidgetが間に挟まり、childでScaffoldを指定している
return MyInheritedWidget(
message: "I am InheritedWidget",
child: Scaffold(
appBar: AppBar(
title: Text(widget.title!),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
// Container->Center->Row->Column->WidgetAの階層で呼び出す
// 階層を深くしたいだけなので、Container~Columnまでに意味はなし
Container(
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [WidgetA()])
]))),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
));
}
}
まず、通常Scaffold
を返すbuild
関数に、MyInheritedWidget
を間に挟んでいます。
次に、結果を出力するWidgetA
の上位にContainer->Center->Row->Columnを入れています。これは、階層を深くしても直接アクセスができることを説明したいだけの実質ダミーのWidgetです。大切なのは末端のWidgetA
です。
最後に、このWidgetA
がMyInheritedWidget
のもつmessage
にアクセスします。
不要部分を減らしたいので、下記の部分は今回は削除しています。
Text('You have pushed the button this many times:')
Text('$_counter',style: Theme.of(context).textTheme.headline4,)
import 'package:flutter/material.dart';
class MyInheritedWidget extends InheritedWidget {
final String message;
// コンストラクタでメッセージと子Widgetを取る
MyInheritedWidget({required this.message, required Widget child})
: super(child: child);
// O(1)でInheritedWidgetを返却
static MyInheritedWidget of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>()
as MyInheritedWidget;
//更新されたかどうかの判定ロジック
@override
bool updateShouldNotify(MyInheritedWidget oldWidget) =>
oldWidget.message != message;
}
こちらがMyInheritedWidget
の定義です。
messageと下位となるWidgetツリーを引数で受け取るコンストラをもっています。
今回はchild
にはScaffold
が入ってきます。
また、staticメソッドのof
をオーバライドしています。
of
メソッドはcontext
を使って、ウィジェットツリーをさかのぼって探すメソッドです。
dependOnInheritedWidgetOfExactType
がO($1$)で処理できます。
ofメソッドについては、よくわからないという方は、BuildContextとofメソッドをご参照ください
import 'package:flutter/material.dart';
import 'package:hello_world/MyInheritedWidget.dart';
class WidgetA extends StatelessWidget {
@override
Widget build(BuildContext context) {
MyInheritedWidget _myInheritedWidget = MyInheritedWidget.of(context);
String message = _myInheritedWidget.message;
return Text(message);
}
}
最後が末端で利用されるWidgetAの実体です。
build
の中で、message
を含むText
を返していますが、そのmessage
をMyInheritedwidget
から持ってきています。
まず、of
メソッドでインスタンスを取得し、その後message
フィールドから値を取り出しています。
上記の例は単なる文字列ですが、serviceオブジェクトなどを共有できるようにしておき、取得後サービスのメソッドを呼び出して活用することもできます。
特徴2:下位ツリーの部分ビルド
Flutterでは、Widgetツリーの再構築や頻繁に行われています。
そこで、以下のような2つのニーズがあります。
- あえて作り直しを無効化させて作り直さない
- 狙い撃ちで部分ツリーを強制的に再構築する
値を変えない例
まず、再構築の動作例を見る前に、値が変わらない例を作っておきます。
String
のmessage
ではなく、int
のcount
を利用します。
Scaffold
の下のCenter
を静的な定義にして作り変わらないようにします。
void main() {/* 変わらないので省略 */ }
class MyApp extends StatelessWidget { /* 変わらないので省略 */ }
class MyHomePage extends StatefulWidget { /* 変わらないので省略 */ }
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!),
),
// 静的に作ったCenterより下のツリーを配置する
body: _widget,
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
上記のように、事前に作っておき、Widgetが変わらないように制御すると、カウントは増えますが、画面は切り替わりません
ほぼ重複なので省略しますが、WidgetAとMyInheritedWidgetのString messageをint counterに変更していますので、ご注意ください。
I/flutter (23418): count:1
I/flutter (23418): count:2
I/flutter (23418): count:3
値を変える例
次に、InheritedWidget
の動作を確認するために、body
を下記のようにして、MyInheritedWidget
を挟んでみます。
//ビルドメソッド以外は同じのため省略
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title!),
),
+ body: MyInheritedWidget(count: _counter, child: _widget),
- //body: _widget,
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
InheritedWidget
のフィールドを介して、状態の伝播が伝わり、Widget
が作り直されています。
では、どうやって変更されたかを認識できるのでしょうか?
その正体が、updateShouldNotifyです。
//ビルドメソッド以外は同じのため省略
import 'package:flutter/material.dart';
class MyInheritedWidget extends InheritedWidget {
final int count;
MyInheritedWidget({required this.count, required Widget child})
: super(child: child);
// O(1)でInheritedWidgetを返却
static MyInheritedWidget of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>()
as MyInheritedWidget;
//更新されたかどうかの判定ロジック
@override
bool updateShouldNotify(MyInheritedWidget oldWidget) {
- //return oldWidget.count != count;
+ return (oldWidget.count != count && count % 2 == 0);
}
}
このupdateShouldNotify
メソッドの戻り値がtrue/false
で更新するかどうかを判断します。
パラメタのoldWidget
で前の状態のWidgetを受け取り、前と今の差分を比較できるようになっています。
ためしに、count
が前回と異なるという条件に加えて、2の倍数の時だけtrueになるように変更してみます。