🏗️

InheritedWidgetの裏側を読む

2022/04/09に公開

InheritedWidgetは、数あるFlutterのWidgetの中でもかなり特殊な部類に入ります。今日では直接扱うことも少なくなりましたが、Providerも内部で使っているように、Flutterの中核となる仕組みの一つでもあります。
InheritedWidgetの役割がどのように実現されているのか、コードを追って理解したいと思います。

InheritedWidgetとは

詳しい説明は多くの記事が存在するのでそちらを見てください。

https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html

https://medium.com/flutter-jp/inherited-widget-37495200d965

https://qiita.com/agajo/items/375d5415cb79689a925c

InheritedWidgetの持つ重要な役割は、

  1. 下位ツリーからO(1)で(定数時間で)アクセスできる
  2. 自身を監視するWidgetに変更を通知する

の2つです。

コードを追うための前提知識

  • InheritedWidgetの一般的な使い方
  • Elementツリーの存在

InheritedWidgetの仕組みはElementツリー内で処理されているため、2つ目の理解はある程度必要です。以下の記事などを参考にしてみてください。

https://zenn.dev/chooyan/books/934f823764db62/viewer/dfe2d1

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);
}

https://github.com/flutter/flutter/blob/c860cba910319332564e1e9d470a17074c1f2dfd/packages/flutter/lib/src/widgets/framework.dart#L1684

InheritedWidget自体の実装は以上です。updateShouldNotifyはInheritedWidgetを継承した際に定義する必要があります。
これを見るとInheritedWidgetの役割はInheritedElementを生成することのみです。

abstract class ProxyElement extends ComponentElement

class InheritedElement extends ProxyElement

https://github.com/flutter/flutter/blob/c860cba910319332564e1e9d470a17074c1f2dfd/packages/flutter/lib/src/widgets/framework.dart#L5196

https://github.com/flutter/flutter/blob/c860cba910319332564e1e9d470a17074c1f2dfd/packages/flutter/lib/src/widgets/framework.dart#L5089

InheritedElementはProxyElementを継承したElementで、さらにProxyElementはStatefulElementやStatelessElementなどと共通してComponentElementを継承しています。
中のメソッドは必要に応じて説明します。

Elementツリーの作成時

InheritedWidgetを利用するために、Elementツリーを構築するときに準備が行われます。

void mount(Element? parent, Object? newSlot) {
  //...
  _updateInheritance();
}

https://api.flutter.dev/flutter/widgets/Element/mount.html

Element.mount()はElementを初めてツリーに入れるときに呼ばれるメソッドです。その最後にElement._updateInheritance()が呼ばれています。
これは名前の通りInheritedWidgetの情報を更新するメソッドで、中身は以下のようになっています。

Map<Type, InheritedElement>? _inheritedWidgets;
//...
void _updateInheritance() {
  assert(_lifecycleState == _ElementLifecycle.active);
  _inheritedWidgets = _parent?._inheritedWidgets;
}

https://github.com/flutter/flutter/blob/c860cba910319332564e1e9d470a17074c1f2dfd/packages/flutter/lib/src/widgets/framework.dart#L4198

親の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;
}

https://github.com/flutter/flutter/blob/c860cba910319332564e1e9d470a17074c1f2dfd/packages/flutter/lib/src/widgets/framework.dart#L5206

ここでやっていることはシンプルです。

  1. 親がMapを持っていたらコピーし、持っていなければ新たに作成
  2. 自身を自身の型を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;
}

https://api.flutter.dev/flutter/widgets/Element/getElementForInheritedWidgetOfExactType.html

中身は単純で、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;
}

https://api.flutter.dev/flutter/widgets/Element/dependOnInheritedWidgetOfExactType.html

こちらも前半は同じく_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;
}

https://api.flutter.dev/flutter/widgets/InheritedElement/updateDependencies.html

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();
}

https://github.com/flutter/flutter/blob/c860cba910319332564e1e9d470a17074c1f2dfd/packages/flutter/lib/src/widgets/framework.dart#L5100

super.updateでWidgetが新しいものに差し替えられます。キャッシュしておいたoldWidgetを使いその後InheritedElement.updated(oldWidget)を呼び出します。

void updated(InheritedWidget oldWidget) {
  if (widget.updateShouldNotify(oldWidget))
    super.updated(oldWidget);
}

https://github.com/flutter/flutter/blob/c860cba910319332564e1e9d470a17074c1f2dfd/packages/flutter/lib/src/widgets/framework.dart#L5336

ここで、開発者の定義したInheritedWidget.updateShouldNotify()を見ていて、trueを返す場合のみProxyElement.updated()を呼ぶようになっています。

void updated(covariant ProxyWidget oldWidget) {
  notifyClients(oldWidget);
}

https://github.com/flutter/flutter/blob/c860cba910319332564e1e9d470a17074c1f2dfd/packages/flutter/lib/src/widgets/framework.dart#L5117

ここは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();
}

https://api.flutter.dev/flutter/widgets/InheritedElement/notifyClients.html

ここで先程監視しているElementの一覧として作ったInheritedElement._dependentsが使われます。最終的にそれぞれのElementのElement.didChangeDependencies()を呼び出すようになっているわけです。

void didChangeDependencies() {
  assert(_lifecycleState == _ElementLifecycle.active); // otherwise markNeedsBuild is a no-op
  assert(_debugCheckOwnerBuildTargetExists('didChangeDependencies'));
  markNeedsBuild();
}

https://api.flutter.dev/flutter/widgets/Element/didChangeDependencies.html

ここが最後です。対象のElementはElement.markNeedsBuild()が呼ばれてneedsBuildフラグが立ち、その後のパイプラインにある再ビルド工程でデータが更新されることになります。

終わり

Discussion