InheritedWidgetの裏側を読む
InheritedWidgetは、数あるFlutterのWidgetの中でもかなり特殊な部類に入ります。今日では直接扱うことも少なくなりましたが、Providerも内部で使っているように、Flutterの中核となる仕組みの一つでもあります。
InheritedWidgetの役割がどのように実現されているのか、コードを追って理解したいと思います。
InheritedWidgetとは
詳しい説明は多くの記事が存在するのでそちらを見てください。
InheritedWidgetの持つ重要な役割は、
- 下位ツリーから
O(1)
で(定数時間で)アクセスできる - 自身を監視するWidgetに変更を通知する
の2つです。
コードを追うための前提知識
- InheritedWidgetの一般的な使い方
- Elementツリーの存在
InheritedWidgetの仕組みはElementツリー内で処理されているため、2つ目の理解はある程度必要です。以下の記事などを参考にしてみてください。
InheritedWidgetとInheritedElement
abstract class InheritedWidget extends ProxyWidget {
const InheritedWidget({ Key? key, required Widget child })
: super(key: key, child: child);
InheritedElement createElement() => InheritedElement(this);
bool updateShouldNotify(covariant InheritedWidget oldWidget);
}
InheritedWidget自体の実装は以上です。updateShouldNotifyはInheritedWidgetを継承した際に定義する必要があります。
これを見るとInheritedWidgetの役割はInheritedElementを生成することのみです。
abstract class ProxyElement extends ComponentElement
class InheritedElement extends ProxyElement
InheritedElementはProxyElementを継承したElementで、さらにProxyElementはStatefulElementやStatelessElementなどと共通してComponentElementを継承しています。
中のメソッドは必要に応じて説明します。
Elementツリーの作成時
InheritedWidgetを利用するために、Elementツリーを構築するときに準備が行われます。
void mount(Element? parent, Object? newSlot) {
//...
_updateInheritance();
}
Element.mount()はElementを初めてツリーに入れるときに呼ばれるメソッドです。その最後にElement._updateInheritance()が呼ばれています。
これは名前の通りInheritedWidgetの情報を更新するメソッドで、中身は以下のようになっています。
Map<Type, InheritedElement>? _inheritedWidgets;
//...
void _updateInheritance() {
assert(_lifecycleState == _ElementLifecycle.active);
_inheritedWidgets = _parent?._inheritedWidgets;
}
親のElementからInheritedWidgetのMapをそのまま受け継いでいます。ではこのMapはどこで書き込まれるのでしょうか?
実は先程のInheritedElementでElement._updateInheritance()が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;
}
ここでやっていることはシンプルです。
- 親がMapを持っていたらコピーし、持っていなければ新たに作成
- 自身を自身の型をkeyにしてMapに追加する
これらから、それぞれのElementの_inheritedWidgetsには自身より上位にあるInheritedElementがMap形式で書き込まれている状態になります。
注意してほしいのは、keyをInheritedWidgetの型としている点です。このためツリー上に同じ型のInheritedWidgetが複数ある場合、一番近いもののみの参照を持っていることになっています。
よく考えるとinheritedWidgetsという名前なのに入っているのはInheritedElementsなの、ややこしい
監視なしでアクセスする
context.getElementForInheritedWidgetOfExactType<アクセスする型>()を呼び出すことで祖先のInheritedElementにアクセスすることができます。
まずBuildContextの実態は対応するElementです。
InheritedElement? getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
return ancestor;
}
中身は単純で、Elementツリーの構築時に更新した_inheritedWidgetsから対応した型のElementを取り出しているだけです。当然アクセス速度はO(1)、定数時間になります。またアクセスするだけなので値の変更の監視なども行われません。
監視ありでアクセスする
context.dependOnInheritedWidgetOfExactType<アクセスする型>()を呼び出すことで祖先のInheritedWidgetにアクセスすることができます。またこちらはそのInheritedWidgetの値が変わったときに通知を受けることができます。
Element.dependOnInheritedWidgetOfExactType()は以下のようになっています。
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;
}
//...
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object? aspect }) {
//...
ancestor.updateDependencies(this, aspect);
return ancestor.widget;
}
こちらも前半は同じく_inheritedWidgetsから対応するInheritedElementを取得しています。異なるのは後半部分で、ancestorが見つかった場合Element.dependOnInheritedElement()を呼び出しています。
dependOnInheritedElement内では、widgetを返す前にancestor.updateDependencies()を呼んで自身のElementを渡しています。
final Map<Element, Object?> _dependents = HashMap<Element, Object?>();
void updateDependencies(Element dependent, Object? aspect) {
setDependencies(dependent, null);
}
void setDependencies(Element dependent, Object? value) {
_dependents[dependent] = value;
}
InheritedElementでこれが呼ばれると、そのElementを_dependentsというMapに格納します(前述の通りaspectに意味はありません)。これでInheritedElementを監視するElementが登録されました。
InheritedWidgetが更新されたとき
何らかの要因でWidgetツリー内のInheritedWidgetが更新(差し替え)され、InheritedElement自体は使い回されるときを考えます。その場合Element.update(newWidget)が更新のため呼ばれます。
InheritedElementはProxyElementでupdateがoverrideされています。
void update(ProxyWidget newWidget) {
final ProxyWidget oldWidget = widget;
assert(widget != null);
assert(widget != newWidget);
super.update(newWidget);
assert(widget == newWidget);
updated(oldWidget);
_dirty = true;
rebuild();
}
super.updateでWidgetが新しいものに差し替えられます。キャッシュしておいたoldWidgetを使いその後InheritedElement.updated(oldWidget)を呼び出します。
void updated(InheritedWidget oldWidget) {
if (widget.updateShouldNotify(oldWidget))
super.updated(oldWidget);
}
ここで、開発者の定義したInheritedWidget.updateShouldNotify()を見ていて、trueを返す場合のみProxyElement.updated()を呼ぶようになっています。
void updated(covariant ProxyWidget oldWidget) {
notifyClients(oldWidget);
}
ここはInheritedElement.notifyClient()を呼ぶだけです。
void notifyClients(InheritedWidget oldWidget) {
assert(_debugCheckOwnerBuildTargetExists('notifyClients'));
for (final Element dependent in _dependents.keys) {
//...
notifyDependent(oldWidget, dependent);
}
}
void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
dependent.didChangeDependencies();
}
ここで先程監視しているElementの一覧として作ったInheritedElement._dependentsが使われます。最終的にそれぞれのElementのElement.didChangeDependencies()を呼び出すようになっているわけです。
void didChangeDependencies() {
assert(_lifecycleState == _ElementLifecycle.active); // otherwise markNeedsBuild is a no-op
assert(_debugCheckOwnerBuildTargetExists('didChangeDependencies'));
markNeedsBuild();
}
ここが最後です。対象のElementはElement.markNeedsBuild()が呼ばれてneedsBuildフラグが立ち、その後のパイプラインにある再ビルド工程でデータが更新されることになります。
終わり
Discussion