勉強していたら InheritedWidget
ってWidget
が出てきたけれど、
一体何なんだろう?
状態管理で使えるらしいけれど、どう使うのか気になるわ!
本記事ではそんな疑問にお答えします。
Provider
やRiverpod
の内部で使われる、状態管理の基礎となるWidget
、 InheritedWidget
について解説します。
基本的な使い方を始めとして、
内部でどんなことが行われているのかについても触れていきます。
今はとても優れた状態管理パッケージがたくさんあるので、
わざわざこのInhertitedWidgetを使うことはないかと思います。
ですが、温故知新という言葉があるように、
昔の、基礎となる優れた考え方を知ることは、
新しいことを発見する足がかりとなるかもしれません。
なので本記事は、初心者の方から理解を深めたい中級者の方まで
有用な記事となっているかと思います。
かなり長い記事となりますが、ぜひ読んでみて下さい!
InheritedWidget の概要と使い方
InheritedWidget
の概要と使い方について解説していきます。
InheritedWidget
はProvider
や Riverpod
の内部でも使われている、
状態管理の基礎となるWidget
です。
このため、Inherited Widget
だけでも状態管理は可能となっています。
「そもそも状態管理って何?」と思われる方がいるかも知れません。
以下で状態管理の課題(InheritedWidget
で解決したい課題)について、
まずは見ていきましょう。
InheritedWidget で解決したい課題
Flutterのコードを書くことにある程度慣れてくると、
Widget の build メソッドの中にWidgetを何度も追加して、
Widgetの依存関係がどんどん深くなって行くかと思います。(以下の図)
課題になるのは、最下層のWidget(水色)で最上部のWidgetが
持つデータ(黄色)を参照したい時です。
どうやって参照すれば良いでしょうか?
一つの方法は、上のWidgetからコンストラクタを使ってデータを受け渡していく方法です。
(以下のコード)
class GrandpasWidget extends StatefulWidget {
const GrandpasWidget({super.key});
@override
State<GrandpasWidget> createState() => _GrandpasWidgetState();
}
class _GrandpasWidgetState extends State<GrandpasWidget> {
int data = 100;
@override
Widget build(BuildContext context) {
return FathersWidget(
data: data,
);
}
}
class FathersWidget extends StatelessWidget {
const FathersWidget({super.key, required this.data});
final int data;
@override
Widget build(BuildContext context) {
return MyWidget(data: data);
}
}
class MyWidget extends StatelessWidget {
const MyWidget({super.key, required this.data});
//・・・
}
この方法だと確実にデータは受け渡せますが、
同じようなコードを何度も書くことになり、ちょっと冗長ですよね。
可能であれば以下のように直接参照したいです。
これを可能にするのがInherited Widget
です。
データをInheritedWidget
に持たせることで、
依存関係がInheritedWidget
の下にあるWidget
ならどこからでも
データを参照できるようになります。
InheritedWidget
はFlutterのSDK の中のWidget
なので、
特別にパッケージをインストール必要はありません。
InheritedWidget
で課題が解決できるなこと、
わかっていただけましたでしょうか。
では、実際のコードにて基本的な使い方を解説していきます。
基本的な使い方
基本的な使い方の概要は以下の通りです。
- データを共有したい
Widget
群の上部で全てと依存関係がある部分に、InheritedWidget
継承クラスを配置する InheritedWidget
継承クラスに用意したメソッドにてデータを参照する
ベースとなるコードはこちらになります。
2つの画面にカウンターが設定されています。
このアプリでは、2つの画面で同じ値を状態として共有したいです。
そのため、依存関係が上のWidgetに状態(データ)をもたせる必要がある、
そんなアプリとなっています。
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({
super.key,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.blue,
title: const Text('First Page'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'ボタンを押した回数',
),
Text(
'ここに回数を表示する',
style: Theme.of(context).textTheme.headline4,
),
],
),
ElevatedButton(
onPressed: () {
Navigator.of(context).push<void>(
MaterialPageRoute(
builder: (context) => const MySecondPage(),
),
);
},
child: const Text('次のページ'),
)
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.add),
),
);
}
}
class MySecondPage extends StatelessWidget {
const MySecondPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.red,
title: const Text('Second Page'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'ボタンを押した回数',
),
Text(
'ここに回数を表示する',
style: Theme.of(context).textTheme.headline4,
),
],
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('前のページ'),
)
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.add),
),
);
}
}
InheritedWidget
継承クラスの配置
まず、InheritedWidgert
継承クラスを用意します。
定義のコードは以下の通りです。
class InheritedCounter extends InheritedWidget {
const InheritedCounter({
super.key,
required this.counter,
//1
required super.child,
});
final int counter;
//2
@override
bool updateShouldNotify(covariant InheritedWidget oldWidget) => true;
}
共通で持たせたいデータのcounter
の部分以外は、InheritedWidget
の継承により必要なコードとなっています。
//1 InheritedWidget
の継承クラスはchild
を引数に設定する必要があります。
//2InheritedWidget
の継承クラスはupdateShouldNotify
メソッドを
オーバーライドする必要があります。
このメソッドは、このInheritedWidget
の継承クラスがリビルドされた際にInheritedWidget
の継承クラスからデータを受け取ったWidget
をリビルドするか否かを
判定するメソッドです。
簡易化のため、今回は常にtrue
を返すとしています。
次に、MyHomePage
とMySecondPage
が共通で依存関係を持っているMyApp
にて、
このInheritedCounter
クラスを配置します。
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const InheritedCounter(
counter: 100,
child: MaterialApp(
home: MyHomePage(),
),
);
}
}
データの参照
InheritedCounter
にて設定したcounter = 100
という値、
これをMyHomePage
、MySecondPage
にて参照するためには、
以下のdependOnInheritedWidgetOfExactType
メソッドを用います。
context.dependOnInheritedWidgetOfExactType<InheritedCounter>()!.counter
ただ、このメソッド、ちょっと長いですよね。
これをもっと短くするためにInheritedCounter
に以下のメソッドを追加しましょう。
class InheritedCounter extends InheritedWidget {
const InheritedCounter({
super.key,
required this.counter,
required super.child,
});
final int counter;
//このメソッドを追加
static InheritedCounter of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<InheritedCounter>()!;
@override
bool updateShouldNotify(covariant InheritedWidget oldWidget) => true;
}
これにより以下のメソッドで参照可能となります。
InheritedCounter.of(context).counter
このメソッドを追加してデータを参照したアプリのコードは以下の通りです。
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class InheritedCounter extends InheritedWidget {
const InheritedCounter({
super.key,
required this.counter,
required super.child,
});
final int counter;
static InheritedCounter of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<InheritedCounter>()!;
@override
bool updateShouldNotify(covariant InheritedWidget oldWidget) => true;
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const InheritedCounter(
counter: 100,
child: MaterialApp(
home: MyHomePage(),
),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({
super.key,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.blue,
title: const Text('First Page'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'ボタンを押した回数',
),
Text(
'${InheritedCounter.of(context).counter}',
style: Theme.of(context).textTheme.headline4,
),
],
),
ElevatedButton(
onPressed: () {
Navigator.of(context).push<void>(
MaterialPageRoute(
builder: (context) => const MySecondPage(),
),
);
},
child: const Text('次のページ'),
)
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.add),
),
);
}
}
class MySecondPage extends StatelessWidget {
const MySecondPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.red,
title: const Text('Second Page'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'ボタンを押した回数',
),
Text(
'${InheritedCounter.of(context).counter}',
style: Theme.of(context).textTheme.headline4,
),
],
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('前のページ'),
)
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.add),
),
);
}
}
コードを実行すると、ちゃんとWidget
の依存関係の上部で設定したcounter = 100
という値を取得できていることがわかります。
データの更新
カウンターアプリとしては、参照だけでなく値の更新もしたいところです。InheritedCounter.of(context).counter++
とすればよさそうですが、これはできません。StatefulWidget
のState
と違い、 InheritedWidget
(というかWidget
)は
一度インスタンスが生成された後、自身を変えることができない、immutable
(不変)の性質を持つからです。
ではどうすればよいでしょうか?
答えは、データを変えることのできるStatefulWidget
と InheritedWidget
を組み合わせる、です。
ここから若干テクニカルなことをします。
一旦組み合わせたコードを見てみましょう。
//3
class _InheritedCounter extends InheritedWidget {
const _InheritedCounter({
required this.data,
required super.child,
});
//4
final MyCounterState data;
@override
bool updateShouldNotify(covariant InheritedWidget oldWidget) => true;
}
class MyCounter extends StatefulWidget {
const MyCounter({
super.key,
required this.child,
});
//5
final Widget child;
//6
static MyCounterState of(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<_InheritedCounter>()!
.data;
}
@override
State<MyCounter> createState() => MyCounterState();
}
//7
class MyCounterState extends State<MyCounter> {
int count = 0;
void increment() => setState(() {
count++;
});
//8
@override
Widget build(BuildContext context) {
return _InheritedCounter(
data: this,
child: widget.child,
);
}
}
//3InheritedWidget
をMyCounter
, MyCounterState
(組み合わせるStatefulWidget
とState
)
からしか参照しなくなるため、プライベート(アンダーバー付き)にします。
//4InheritedWidget
で保持するデータを、MyCounterState
(組み合わせるStatefulWidget
のState
)とします。
//5MyCounter Widget
では受け取ったWidget
を_InheritedCounter
でラップし返す、
という処理を行うためWidget
を受け取るよう設定します。
//6InheritedCounter
をプライベートにしたため、InheritedCounter
にあったof
メソッドをMyCounter
に移動しています。
//7
ofメソッドで MyCounterState
を返すため、MyCounterState
をパブリック(アンダーバーなし)にしています。
//8child
で受け取ったWidget
をbuild
メソッドで返す際に_inheritedCounter
で囲んで返します。
これによりMyCounter
より依存関係が下のWidget
はInheritedWidget
で
囲まれることとなります。_inheritedCounter
に設定したthis
はbuild
メソッドを実行しているMyCounterState
自身を表しています。
先程InheritedWidget
継承クラスで囲んでいた代わりに、
作成したMyCounter
で囲います。
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MyCounter(
child: MaterialApp(
home: MyHomePage(),
),
);
}
}
値の更新はMyCounterState
の メソッド(increment
)で行います。
更新の仕組みとしては以下のようになります。
MyCounter.of(context).increment()
でMyCounterState
のcounter
の値を更新、setState
が実行されるsetState
によりMyCounterState
がリビルドされ、
値の更新されたMyCounterState
が_inheritedCounter
に渡される_inheritedCounter
がデータの変化を感知し、_inheritedCounter
のデータを観測しているWidg
etにリビルドをリクエストする_inheritedCounter
のデータを観測しているWidg
etがリビルドされ
更新されたデータを取得、画面に更新されたデータが表示される
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class _InheritedCounter extends InheritedWidget {
const _InheritedCounter({
required this.data,
required super.child,
});
final MyCounterState data;
@override
bool updateShouldNotify(covariant InheritedWidget oldWidget) => true;
}
class MyCounter extends StatefulWidget {
const MyCounter({
super.key,
required this.child,
});
final Widget child;
static MyCounterState of(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<_InheritedCounter>()!
.data;
}
@override
State<MyCounter> createState() => MyCounterState();
}
class MyCounterState extends State<MyCounter> {
int count = 0;
void increment() => setState(() {
count++;
});
@override
Widget build(BuildContext context) {
return _InheritedCounter(
data: this,
child: widget.child,
);
}
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MyCounter(
child: MaterialApp(
home: MyHomePage(),
),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.blue,
title: const Text('First Page'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'ボタンを押した回数',
),
Text(
'${MyCounter.of(context).count}',
style: Theme.of(context).textTheme.headline4,
),
],
),
ElevatedButton(
onPressed: () {
Navigator.of(context).push<void>(
MaterialPageRoute(
builder: (context) => const MySecondPage(),
),
);
},
child: const Text('次のページ'),
)
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
MyCounter.of(context).increment();
},
child: const Icon(Icons.add),
),
);
}
}
class MySecondPage extends StatelessWidget {
const MySecondPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.red,
title: const Text('Second Page'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'ボタンを押した回数',
),
Text(
'${MyCounter.of(context).count}',
style: Theme.of(context).textTheme.headline4,
),
],
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('前のページ'),
)
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
MyCounter.of(context).increment();
},
child: const Icon(Icons.add),
),
);
}
}
改良点
Flutter Performance で+ボタンを押した時のリビルドの状況を見てみましょう。
画面的に変化している部分はカウンター部分のText
だけですが、
変化していないScaffold
などもリビルドされていることがわかります。
このリビルドを最小限に押さえるためにはどうすればよいでしょうか?
手順は2つです。
Builder
を使ってリビルドされるWidgetを制限するFloating Action Button
にて『InheritedWidget
を監視しているものリスト』に
登録されないようにする
Builder
を使ってリビルドされるWidgetを制限する
MyCounter.of(context).〜
(正確にはdependOnInheritedWidgetOfExactType
)を使うと、
引数に使用したcontext
が、
『Inherited Widget
を監視しているものリスト』に登録されます。
Inherited Widget
が更新されると、このリストに紐付いたWidgetがリビルドされる、
という仕組みとなっています。
Builder
で囲まない場合だと、MyCounter.of(context).〜
で引数に使用したcontext
は、MyHomePage
Widget
のbuild
メソッドのcontext
のため、Inherited Widget
が更新されるとMyHomePage
Widget
が更新されてしまう訳です。
以下の画像のようにBuilder Widget
を使ってText Widget
を切り出すと、MyCounter.of(context).〜
で引数に使用したcontext
はBuilder Widget
のcontext
となるため、
リビルドされるWidget
をBuilder
以下に制限することができます。
Floating Action Button
にて『InheritedWidget
を監視しているものリスト』に
登録されないようにする
タップすることデータを増加させる Floating Action Button
でもMyCounter.of(context).〜
(この内部でのdependOnInheritedWidgetOfExactType
)
が使われているため、
タップすると使用しているcontext
が『InheritedWidget
を監視しているものリスト』
に登録されてしまいリビルドの範囲が拡大されてしまいます。
そこで、タップされたときにはdependOnInheritedWidgetOfExactType
ではなく、getElementForInheritedWidgetOfExactType
を使うようにします。
具体的には、MyCounter
のof
メソッドを以下のように書き換えます。
static MyCounterState of(BuildContext context, {bool rebuild = true}) {
return rebuild
? context.dependOnInheritedWidgetOfExactType<_InheritedCounter>()!.data
: (context
.getElementForInheritedWidgetOfExactType<_InheritedCounter>()!
.widget as _InheritedCounter)
.data;
}
引数に、リビルド対象とするか否かを判定するフラグを持たせ、InheritedWidget
の更新にあわせてリビルドしたいときにはdependOnInheritedWidgetOfExactType
を、
リビルドしたくない際にはgetElementForInheritedWidgetOfExactType
を
使えるようにしています。
具体的なFloating Action Button
の実装は以下のようになります。
floatingActionButton: FloatingActionButton(
onPressed: () {
MyCounter.of(context, rebuild: false).increment();
},
child: const Icon(Icons.add),
),
以上が改善内容となります。
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class _InheritedCounter extends InheritedWidget {
const _InheritedCounter({
required this.data,
required super.child,
});
final MyCounterState data;
@override
bool updateShouldNotify(_InheritedCounter oldWidget) => true;
}
class MyCounter extends StatefulWidget {
const MyCounter({
super.key,
required this.child,
});
final Widget child;
static MyCounterState of(BuildContext context, {bool rebuild = true}) {
return rebuild
? context.dependOnInheritedWidgetOfExactType<_InheritedCounter>()!.data
: (context
.getElementForInheritedWidgetOfExactType<_InheritedCounter>()!
.widget as _InheritedCounter)
.data;
}
@override
State<MyCounter> createState() => MyCounterState();
}
class MyCounterState extends State<MyCounter> {
int count = 0;
void increment() => setState(() {
count++;
});
@override
Widget build(BuildContext context) {
return _InheritedCounter(
data: this,
child: widget.child,
);
}
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MyCounter(
child: MaterialApp(
home: MyHomePage(),
),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.blue,
title: const Text('First Page'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'ボタンを押した回数',
),
Builder(
builder: (context) {
return Text(
'${MyCounter.of(context).count}',
style: Theme.of(context).textTheme.headline4,
);
},
),
],
),
ElevatedButton(
onPressed: () {
Navigator.of(context).push<void>(
MaterialPageRoute(
builder: (context) => const MySecondPage(),
),
);
},
child: const Text('次のページ'),
)
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
MyCounter.of(context, rebuild: false).increment();
},
child: const Icon(Icons.add),
),
);
}
}
class MySecondPage extends StatelessWidget {
const MySecondPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.red,
title: const Text('Second Page'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'ボタンを押した回数',
),
Builder(
builder: (context) {
return Text(
'${MyCounter.of(context).count}',
style: Theme.of(context).textTheme.headline4,
);
},
),
],
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('前のページ'),
)
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
MyCounter.of(context, rebuild: false).increment();
},
child: const Icon(Icons.add),
),
);
}
}
結果、カウントアップした際のリビルドの対象を少なくすることができました。
以上がInherited Widgetの概要でした。
次は具体的にInherited Widgetがどんなことをしているのか、内部の仕組みを追っていきます。
Inherited Widget の仕組み
Inherited Widget
の仕組みについて見ていきましょう。
この章を読めば、『何故dependOnInheritedWidgetOfExactType
が優れているのか?』
がわかるはずです。
そのためにまず基礎知識としてElement
とは何か?について簡単に解説します。
Elementとは何か?
Widget
が描画される時、内部では、3種類の要素が活躍しています。Widget , Element, RenderObject
です。
それぞれの役割は以下のようになっています。
Widget
: そのWidget
の設定を管理するものElement
: そのWidget
のツリー上での位置やライフサイクルを管理するものRenderObject
: そのWidget
のサイズやレイアウト、描画を管理するもの
特にElement
はそのWidget
についてどんな祖先や子がいるのかの依存関係や、
Widgetが更新された時どのように更新、再構築するかなどのライフサイクルを管理する、
重要な役割を担っています。
InheritedWidget
もこのElement
と深く関わっています。
次の節で見てみましょう。
InheritedElement の継承
Element
のクラスのコードの中に、_inheritedWidgets
、というプロパティがあります。
これはその名の通り、先祖のInherited Widget
のElement
(InheritedElement
)を保管しているMap
です。
InheritedElement
がmount
される(ツリー上に配置される)際に、
自身が_inheritedWidgets
に登録されます。
class InheritedElement extends ProxyElement {
/// Creates an element that uses the given widget as its configuration.
InheritedElement(InheritedWidget widget) : super(widget);
final Map<Element, Object?> _dependents = HashMap<Element, Object?>();
@override
void _updateInheritance() {
assert(_lifecycleState == _ElementLifecycle.active);
final Map<Type, InheritedElement>? incomingWidgets = _parent?._inheritedWidgets;
if (incomingWidgets != null)
_inheritedWidgets = HashMap<Type, InheritedElement>.of(incomingWidgets);
else
_inheritedWidgets = HashMap<Type, InheritedElement>();
_inheritedWidgets![widget.runtimeType] = this;
}
Element
がmount
される度に、親から子へ継承されるため、
すべてのInheritedElement
の子のElement
は、_inheritedWidgets
にて
祖先の
を持っていることとなります。InheritedElement
ざっくばらんに言えば、Element
は祖先のInheritedElement
の情報を保持している、ということです。
dependOnInheritedWidgetOfExactTypeについて
dependOnInheritedWidgetOfExactType
メソッドはBuildContext
のメソッドとして定義され、BuildContext
の実装であるElement
にて実装されています。
実装コードは以下のようになっています。
@override
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
if (ancestor != null) {
return dependOnInheritedElement(ancestor, aspect: aspect) as T;
}
_hadUnsatisfiedDependencies = true;
return null;
}
4行目にて、_inheritedWidgets
から指定した型のInheritedWidget
のInheritedElement
を取得し、ancestor
に格納していることがわかります。
dependOnInheritedElement
メソッドは処理後にancestor
の持つWidget
を返すので、
結論、_inheritedWidgets
で保管していた祖先のInheritedElement
、
並びにInheritedWidet
を取得するメソッドとなっています。
このメソッドのすごいところは、祖先のInheritedWidget
を取得するのに、
がとても簡単なことです。
祖先のInheritedElement
をすべての子のElement
で保管していて、
その中から探すだけなので、どれだけ子が多かったとしても(ツリーが深かったとしても)
とても簡単に取得できるのです。
(計算量がO(1)で済みます。)
getElementForInheritedWidgetOfExactTypeについて
本記事で祖先のInheritedWidget
を取得する方法として紹介したものに、getElementForInheritedWidgetOfExactType
がありました。
こちらについても実装を見てみましょう。
@override
InheritedElement? getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
return ancestor;
}
こちらも同様に_inheritedWidgets
から欲しいInheritedWidget
のInheritedElement
を取得していることがわかります。
dependOnInheritedWidgetOfExactType
との違いは、dependOnInheritedElement
メソッドを間に噛ませているか否かです。
では、dependOnInheritedElement
メソッドの実装を見てみましょう。
@override
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object? aspect }) {
assert(ancestor != null);
_dependencies ??= HashSet<InheritedElement>();
_dependencies!.add(ancestor);
ancestor.updateDependencies(this, aspect);
return ancestor.widget as InheritedWidget;
}
ポイントは、6行目のancestor.updateDependencies(this, aspect)
です。
この行ではancestor
(祖先のInheritedElement
)のupdateDependencies
メソッドを呼び出し、
子孫である自身を引数に与えています。
このメソッドにより、祖先のInheritedElement
内の_dependents
に子孫である自身が登録されます。
登録されたものはInheritedElement(正確には継承元のProxyElement
)で実装されているupdate
メソッドが発火した際に、子孫であるElementのWidgetをリビルドするように設定します。
まとめると、dependOnInheritedElement
メソッドは呼び出しているElement
を、InheritedWidget
に関係するElement
の集合
(前述の『『InheritedWidget
を監視しているものリスト』)に登録するメソッドです。
これが、getElementForInheritedWidgetOfExactType
では呼び出されていないため、
こちらでは監視対象リストに入らないということがわかります。
以上がInheritedWidget
の仕組みの話でした。
まとめ
本記事ではInheritedWidget
の基本的な使い方を始めとして、
内部でどんなことが行われているのかについて解説していきました。
かなり長い記事でしたが、いかがだったでしょうか?
現在は優れた状態管理パッケージが様々作成されているため、
今回の記事の内容が直接役に立つことはあまりないかもしれません。
ですが、最初に述べたように、昔のやり方を知ること、
基礎を知ることは、Flutter力の底上げとしてとても良いことだと考えます。
興味がある方は、Riverpod や Providerの内部実装コードのリーディングに挑戦してみて下さい。
今回記載した内容が多数見つかり興味深いはずです。
本記事があなたのアプリ開発の一助となれば幸いです。
参考
編集後記(2022/7/6)
本記事はInheritedWidgetについての記事でした。
過去最高に学ぶことが多く、労力を注いだ記事となりました。
いかがだったでしょうか?
本記事がすぐに誰かの役に立つことはあまりないかもしれません。
ただこのような記事を書く意義はあるかと思っています。
すぐに役に立たない研究論文を書くのは有用なことか?
という議論を耳にしたことがあります。
その研究論文を読んで誰かが新たな発見をし論文を書き、
また異なる誰かがその論文を読んで新たな発見をする、
といった形で連鎖することがある、だから有用だ、という意見が寄せられていました。
本記事もこのような形で誰かの知識の糧になれば良いと思っています。
高望みかもしれませんが、本記事があなたの、
ないしまだ見ぬ誰かのアプリ開発の一助となれば幸いです。
週刊Flutter大学では、Flutterに関する技術記事、Flutter大学についての紹介記事を投稿していきます。
記事の更新情報はFlutter大学Twitterにて告知します。
ぜひぜひフォローをお願いいたします。