Flutter 🇯🇵
Published in

Flutter 🇯🇵

InheritedWidget を完全に理解する 🎯

Flutterフレームワーク・providerパッケージを支える重要なWidget

TL;DR

InheritedWidgetとは何かというと、ざっくり以下のように説明できます。

  • 必要な時に限定して変更を下位ツリー内の特定のWidgetのみに伝播する(リビルドを発生させる)ことができる
  • その特性を活かすためにStatefulWidgetとセットで使われることが多い
  • 最近はアプリケーションコードで直接使うことは少なくなってきて、代わりにそれをラップしたproviderパッケージなど経由で使うことが多い
  • 必要な時に限定して変更を下位ツリー内の特定のWidgetのみに伝播する(リビルドを発生させる)ことができる

下位ツリーから O(1) でアクセスできる特性を理解する

FlutterのUIはWidgetツリーで構成されます(厳密にはElementおよびRenderObjectツリーが関係しますが)が、上位ツリーおよび下位ツリーへのアクセスも可能です。

  • 下位ツリーからのアクセス: BuildContextのfindAncestorWidgetOfExactTypeで上位ツリーの所望の型のWidgetにアクセス

dependOnInheritedWidgetOfExactType

  • これが呼ばれると引数に渡ってきたBuildContext(Element)が登録されてWidgetに変更があった時に下位ツリーのリビルドを発生させられる(updateShouldNotify で間引き可能)
  • この監視の仕組みを実現するため、didChangeDependencies 以降のタイミングでしか呼べない

getElementForInheritedWidgetOfExactType

  • 監視はせずに単に直近のInheritedWidgetのその時点でのElementを取得するだけ
  • initState タイミングでも呼べる
実行結果
  • さらに、慣例的に of メソッドを生やして上述のO(1)でのアクセス方法を提供
  • 変更監視せずにアクセスだけするgetElementForInheritedWidgetOfExactTypeを利用してさらにInheritedWidgetのupdateShouldNotifyは常にfalseを返すようにして変更伝播機能を一切無効化(詳しくは後述)
  • _Message をその _Inherited で包みつつmessageプロパティには 🐶 をセット
  • _Message は引数無しで上位ツリーから直接値を渡されていないが、 _Inheritedof メソッド経由でセットされたmessageにアクセスできる
  • InheritedWidgetの保持するデータが変更された時にそれに追従することができる(次で述べる内容)

必要な時に限定して変更を下位ツリー内の特定のWidgetのみに伝播

次に、InheritedWidgetの変更伝播の仕組みについてです。ここがけっこうややこしく、かつ勘違いしやすいところです。

  • StateのフィールドにキャッシュされたWidget
  • 上位ツリーからchild引数などで渡されて使い回されるWidget
  • 3の倍数: Message: Fizz
  • 5の倍数: Message: Buzz
  • それ以外: Message: -
実行結果とリビルドの様子
  • 監視する方の dependOnInheritedWidgetOfExactType でアクセス
  • updateShouldNotify ではmessageが変わった時のみ true を返すようにする(このため、setStateされても Message: - のまま不変なタイミングでは _Message Widgetのリビルドはされない)
  • _Message Widgetには const を指定して無駄な巻き込まれリビルドを抑制

もう少ししっかり無駄なリビルドを抑制

上のGIFを見て分かる通り、 _HomePageState の粒度が大きく Scaffold が毎回リビルドされているなど、まだ無駄があります。上述の通り、通常そこまで神経質になることはないものの、以下の方針で書き換えて StatefulWidget + InheritedWidget部分を状態管理に必要なところだけに絞って、UI表示に必要な部分を別Widgetとして切り出して child 引数で外から受け取るようにするとリビルドを最小限にできます。

providerパッケージとは?

Pragmatic State Management in Flutter (Google I/O’19) で紹介・推奨されたことで注目を集めたパッケージです。

Pragmatic State Management in Flutter (Google I/O’19)
  • InheritedWidget を StatefulWidget で包んで様々な状態管理を簡単にできるようにしてくれるもの

InheritedWidgetを使いやすくしたラッパー

まず、ここから例を元に簡単に説明します。上のFizzBuzzサンプルのInheritedWidgetの絡むところをproviderで書き換えると次のようになります。

  • 値へのアクセス・監視も Provider.of だけで良い

InheritedWidgetをStatefulWidgetで包んで様々な状態管理を簡単にできるようにしてくれるもの

というわけで、同じくFizzBuzzサンプルをさらに書き換えていきます。

  • 状態管理としては、ValueNotifierを継承した _FizzBuzz クラスを用意して、 providerパッケージの ChangeNotifierProvider で包んで下位ツリーに受け渡す(このbuilderで初期化されたあとProvider内部のStatefulWidgetで _FizzBuzz インスタンスは保持される)
  • この例のようにChangeNotifierProviderのデフォルトコンストラクタを使った場合、builder引数に渡したValueNotifierなど(ChangeNotifierのサブクラス)のdisposeメソッドを適切なタイミングで自動的に呼んでくれるのでメモリリークの明示的なケアが不要
  • 更新する際に increment() メソッドを呼ぶ際は、Provider.of<_FizzBuzz>(context, listen: false) のように監視は抑制した方がパフォーマンス的にベター
  • Provider.of<_FizzBuzz>(context).message で表示メッセージを監視
  • setStateが呼ばれている箇所が消えたが、それはValueNotifierのvalueが変更されると内部的にnotifyListeners()が呼ばれて、それをChangeNotifierProviderが検知してProvider.ofで監視しているWidgetに伝えて(内部のStateでsetStateして)リビルドされるため
  • また、このsetStateはProviderの内部で呼ばれているため、child引数として外から渡された _HomePage は使い回されるので const 指定の有無に関わらず巻き込まれビルドのようなことは起こらずに済む

InheritedWidget・StatefulWidgetとproviderパッケージの関係を整理

以上の説明で大体掴めたかと思いますが、providerはFlutterフレームワークに含まれるInheritedWidget・StatefulWidgetの以下の機能を簡単に利用できるようにするための便利なラッパーパッケージです。

  • B: InheritedWidgetによる変更伝播
  • C: StatefulWidgetによる状態・リソース管理

余談

個人的には、2018年12月頃にBLoCの受け渡し用に扱いやすくInheritedWidgetをラップしたクラスが欲しいと思ってパッケージなど探している時にproviderパッケージが目に入ったので、まだまだ無名の初期から知っていました。その時は、かなり機能に乏しく単なるInheritedWidgetのGenerics版みたいな感じでBLoC用にそのまま使えるものではなかったので、今のProviderのBLoC特化版とも言える bloc_provider を自作しました。

--

--

Flutterに関する日本語記事を書いていきます🇯🇵

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store