InheritedWidgetの目的と使い方【Flutter】

# まえがき

`InheritedWidget`の使い方の基本をまとめます！

Flutterを勉強していてよくわからなくなるポイントの一つがこの`InheritedWidget`だと思います。

筆者自身、これを理解するのにかなり時間がかかったので、「こういう説明があったらよかったな」と思う説明を書きます。


なお、以下では、`InheritedWidget`クラスを継承したクラスのことを単に`InheritedWidget`と呼びますね。他の`StatefulWidget`なども同様です。


## 前提知識


FlutterアプリがWidgetのツリー構造でできていることを知っていて、`StatefulWidget`と`setState`は使えるものとします。

`BuildContext`の知識も少しだけ要りますが、「Widgetツリー内での位置を表現するもの」程度の理解で十分です。

それについてはこちらをお読みください。

[Flutterのcontextの「お気持ち」を理解する](https://qiita.com/agajo/items/93d75aafe87bdc7b2026)

## 記事内の例の実行方法

該当Widgetを`MaterialApp`で包んで`runApp`に渡すか、GitHubのrepositoryからプロジェクトをダウンロードして実行してください。

それぞれの例がページ別にわかれたアプリになっています。

https://github.com/agajo/learn_inherited_widget


# `InheritedWidget`の存在理由

`InheritedWidget`の目的は、次の2つです。

 - Widgetツリーの下層の`BuildContext`から、プロパティに`O(1)`でアクセスすること
 - プロパティが変化した時に下層の`BuildContext`に更新を伝えてリビルドさせること

一つずつ説明します。

# Widgetツリーの下層の`BuildContext`から、プロパティに`O(1)`でアクセスする

この`O`はランダウの記号と言います。ゼロではなくてオーです。

`O(1)`というのはアクセスにかかる時間が何に依存して増えるかを表す表現なのですが、`O(1)`ということは、そのアクセス時間が何にも依存しない(つまり増えない。常に最速でアクセスできる)ことになります。

[ランダウの記号#無限大における漸近挙動と計算量の見積り - Wikipedia](https://ja.wikipedia.org/wiki/%E3%83%A9%E3%83%B3%E3%83%80%E3%82%A6%E3%81%AE%E8%A8%98%E5%8F%B7#%E7%84%A1%E9%99%90%E5%A4%A7%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8B%E6%BC%B8%E8%BF%91%E6%8C%99%E5%8B%95%E3%81%A8%E8%A8%88%E7%AE%97%E9%87%8F%E3%81%AE%E8%A6%8B%E7%A9%8D%E3%82%8A)

## `dependOnInheritedWidgetOfExactType`を使え！

`BuildContext`には[dependOnInheritedWidgetOfExactType](https://api.flutter.dev/flutter/widgets/BuildContext/dependOnInheritedWidgetOfExactType.html)というメソッドがあります。

`context.dependOnInheritedWidgetOfExactType<Hoge>()`のように書くと、その`context`からWidgetツリーをさかのぼった時に一番近くにある`Hoge`という`InheritedWidget`を返します。

その時の計算時間は`O(1)`です。つまり最速です。


## 基本的に`findAncestorStateOfType`は使うな！
`BuildContext`には[findAncestorStateOfType](https://api.flutter.dev/flutter/widgets/BuildContext/findAncestorStateOfType.html)というメソッドもあり、同じように`BuildContext`からWidgetツリーをさかのぼって一番近くにある、指定した型の`State`を返すのですが、その時の計算時間は`O(n)`です。

この`n`は`BuildContext`から実際に返る`State`までの、Widgetツリーの深さを表します。つまりWidgetツリーが深くなると、それに比例して計算に時間がかかるようになり、アプリが重くなっていくのです。

## 使用例

`InheritedWidget`は、`StatelessWidget`や`StatefulWidget`と同様に、`InheritedWidget`クラスを`extends`したクラスを自分で作って使います。

この例では`MessageData`という`InheritedWidget`を作っています。

`updateShouldNotify`については後ほど説明しますので今はスルーしてください。

下記の`MainWidget()`を、`MaterialApp`で覆って`runApp`に渡します。

```dart:just_access.dart
import 'package:flutter/material.dart';

// InheritedWidgetを作ります。
class MessageData extends InheritedWidget {
  const MessageData({Key key, @required Widget child})
      : assert(child != null),
        super(key: key, child: child);

  // InheritedWidgetのプロパティは変更不可能なので必ずfinal
  final String message = 'InheritedWidgetが保持してるメッセージだよ〜〜〜〜';

  // updateShouldNotifyについては後で説明します。
  @override
  bool updateShouldNotify(MessageData old) {
    return true;
  }
}

class MainWidget extends StatelessWidget {
  const MainWidget();
  @override
  Widget build(BuildContext context) {
    // Widgetツリーの上層部に、作ったInheritedWidgetを配置
    return MessageData(
        child: Scaffold(
      appBar: AppBar(),
      body: WidgetA(),
    ));
  }
}

class WidgetA extends StatelessWidget {
  const WidgetA();
  @override
  Widget build(BuildContext context) {
    // InheritedWidgetからO(1)でmessageを取得！！
    final message =
        context.dependOnInheritedWidgetOfExactType<MessageData>().message;
    return Text(message);
  }
}
```

`WidgetA`内で、上層の`InheritedWidget`から`message`が取得され、表示されます。

![スクリーンショット 2021-01-03 0.18.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/85139/4f06aac1-363d-7670-cc7f-2775584f919a.png)




## Widgetツリー内で`InheritedWidget`を上書きする

Widgetツリー内の一部でだけ異なる`MessageData`を使いたい時は、もう一度`MessageData`Widgetを配置すればいいです。

[Theme](https://api.flutter.dev/flutter/material/Theme-class.html), [MediaQuery](https://api.flutter.dev/flutter/widgets/MediaQuery-class.html), [Directionality](https://api.flutter.dev/flutter/widgets/Directionality-class.html)など多くのFlutter Widgetがこの性質を持っていますね。実はそこでも`InheritedWidget`が利用されています。


次の例を見てください。


`MessageData`は2箇所配置されていますが、より呼び出し元の`BuildContext`に近い方から`message`が取得されていることを確認してください。

`MessageData`のプロパティ`message`はコンストラクタで外から受け取れるようにしました。


```dart:override_message.dart
import 'package:flutter/material.dart';

// InheritedWidgetを作ります。
class MessageData extends InheritedWidget {
  const MessageData({
    Key key,
    @required Widget child,
    @required this.message,
  })  : assert(child != null),
        super(key: key, child: child);

  // 今回はmessageをコンストラクタで受け取ります。
  final String message;

  @override
  bool updateShouldNotify(MessageData old) {
    return true;
  }
}

class MainWidget extends StatelessWidget {
  const MainWidget();
  @override
  Widget build(BuildContext context) {
    return MessageData(
        message: 'メッセージその1だよ',
        child: Scaffold(
          appBar: AppBar(),
          // 内容を変えたMessageDataウィジェットを挟み込みます
          body: MessageData(
            message: 'メッセージその2ですよ〜〜〜',
            child: WidgetA(),
          ),
        ));
  }
}

class WidgetA extends StatelessWidget {
  const WidgetA();
  @override
  Widget build(BuildContext context) {
    final message =
        context.dependOnInheritedWidgetOfExactType<MessageData>().message;
    return Text(message);
  }
}
```

![スクリーンショット 2021-01-03 0.33.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/85139/c4394d18-a11c-67ba-fe0f-0d53c4f78044.png)



# プロパティが変化した時に下層の`BuildContext`に更新を伝えてリビルドさせる

`InheritedWidget`のインスタンスは、中身を変更できません。

にもかかわらず**更新を伝播**するとは、一体どういうことなのでしょう？変更不可能なのに更新するとは？？

筆者は以前、この部分で混乱していました。

答えはこうです。

**`InheritedWidget`のインスタンスを何度も作り直して置き換える**


状態を更新可能な`StatefulWidget`などと組み合わせて、Widgetツリー内にある`InheritedWidget`のインスタンスを作り直して再配置します。

プロパティの異なる`InheritedWidget`を作って置き直せば、プロパティの変更が、それに依存する`BuildContext`に伝播され、リビルドされるわけです。

## 使用例

実際の例で見てみましょう。

`StatefulWidget`を使って、1秒に1回、プロパティを1ずつ増やした`InheritedWidget`を作って再配置します。作る`InheritedWidget`の名前は`CountData`としています。

`InheritedWidget`の`child`に渡すWidgetは、変更せず、同じインスタンスを毎回渡すことにします。



```dart:update_count.dart
import 'dart:async';

import 'package:flutter/material.dart';

// InheritedWidgetはmessageではなくcountを保持することにします。
class CountData extends InheritedWidget {
  const CountData({Key key, @required Widget child, @required this.count})
      : assert(child != null),
        super(key: key, child: child);

  final int count;

  @override
  bool updateShouldNotify(CountData old) {
    return true;
  }
}

// StatefulWidgetを使います。
class MainWidget extends StatefulWidget {
  MainWidget();
  // InheritedWidgetのchildには、毎回このインスタンスを渡します。
  final Widget child = Scaffold(
    appBar: AppBar(),
    body: WidgetA(),
  );

  @override
  _MainWidgetState createState() => _MainWidgetState();
}

class _MainWidgetState extends State<MainWidget> {
  int timeCount = 0;
  Timer timer;

  @override
  void initState() {
    super.initState();
    // Stateの初期化時に、毎秒setStateを呼ぶタイマーを起動します。
    timer = Timer.periodic(Duration(seconds: 1), (timer) {
      setState(() {
        timeCount++;
      });
    });
  }

  @override
  void dispose() {
    // initStateでタイマーを起動・disposeでタイマーを破棄。この2つはセットです。
    timer.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    // タイマーにより毎秒setStateが呼ばれるので、このbuildメソッドが毎秒呼ばれます。
    // その際、このCountDataコンストラクタでCountDataのインスタンスが毎回作り直されます。
    // 渡す引数が異なるので、作られるCountDataのプロパティは毎回異なります。
    return CountData(
      count: timeCount,
      // StateではなくStatefulWidgetが保持しているchildを渡します。
      // つまり、毎秒変更されるインスタンスはCountDataだけであり、他は変化しません。
      child: widget.child,
    );
  }
}

class WidgetA extends StatelessWidget {
  const WidgetA();
  @override
  Widget build(BuildContext context) {
    // ここでこのWidgetAのBuildContextがCountDataの変更通知対象として登録されるので、
    // CountDataのインスタンスが置き換わるたびにこのbuildメソッドが呼ばれます。
    final count = context.dependOnInheritedWidgetOfExactType<CountData>().count;
    return Text('count: $count');
  }
}
```

![Jan-03-2021 20-36-01](https://user-images.githubusercontent.com/12369062/103477595-c3190f80-4e03-11eb-859d-605233ff1606.gif)


## `updateShouldNotify`で更新を間引きする

`InheritedWidget`のよくわからないポイントの一つ、`updateShouldNotify`です。

ここまでの説明で、`InheritedWidget`のインスタンスの置き換えが行われる際に更新が伝播されるようなことを言いましたが、

実は、実際に更新が伝播されるのは`updateShouldNotify`が`true`を返した時だけです。

インスタンスの置き換えの際、古い方のインスタンスも手元に手に入るので、比較して、更新通知するか制御可能です。

例えば先程の例で、更新を2回に1回にする(つまり、`count`を2で割った商が変化した時だけ更新する)場合、`updateShouldNotify`は次のように書けます。


```dart
  @override
  bool updateShouldNotify(CountData old) {
    return count ~/ 2 != old.count ~/ 2;
  }
```

他は先程の例と全く同じようにすると、次のような動きになります。

![Jan-03-2021 20-40-54](https://user-images.githubusercontent.com/12369062/103477617-02476080-4e04-11eb-8b03-46dd04b1a70b.gif)

`updateShouldNotify`の中身を、場合によって都合のいいように実装すればいいわけですね！


# 慣例として、`of`を実装する

ここまで、動きの理解を重視して、[BuildContext.dependOnInheritedWidgetOfExactType](https://api.flutter.dev/flutter/widgets/BuildContext/dependOnInheritedWidgetOfExactType.html)を直接呼んでいましたが、

Flutterの慣例として、このメソッドは直接呼ばず、`InheritedWidget`に`of`というstaticメソッドを実装して、そこから呼ぶのが普通です。

## 例

最初の`MessageData`の例ですと、このようになります。

```dart:implement_of.dart
import 'package:flutter/material.dart';

class MessageData extends InheritedWidget {
  const MessageData({Key key, @required Widget child})
      : assert(child != null),
        super(key: key, child: child);

  final String message = 'InheritedWidgetが保持してるメッセージだよ〜〜〜〜';

  @override
  bool updateShouldNotify(MessageData old) {
    return true;
  }

  // ofを実装します！！！
  static MessageData of(BuildContext context) =>
      context.dependOnInheritedWidgetOfExactType<MessageData>();
}

class MainWidget extends StatelessWidget {
  const MainWidget();
  @override
  Widget build(BuildContext context) {
    return MessageData(
        child: Scaffold(
      appBar: AppBar(),
      body: WidgetA(),
    ));
  }
}

class WidgetA extends StatelessWidget {
  const WidgetA();
  @override
  Widget build(BuildContext context) {
    // of経由で呼びます！！！
    // final message = context.dependOnInheritedWidgetOfExactType<MessageData>().message;
    final message = MessageData.of(context).message;
    return Text(message);
  }
}
```

こうすると、`Theme`などで見かけた`Theme.of(context)`という見た目にそっくりになりましたね。

以上で`InheritedWidget`の基本的な使い方の説明を終わります。


# (応用)下層から更新する場合はどうするの？

`InheritedWidget`を、アプリの状態管理にガッツリ使うことを考えます。

先程のタイマーを使った例では、更新が`StatefulWidget`の`State`内部から自発的に発生していました。

一方実際のアプリでは、Widgetツリー下層にある`RaisedButton`が押されたら、上層で保持している状態を更新する、という動きがよくあります。

となると、Widgetツリーの下層から上層の状態を更新するメソッドを呼ぶ必要があるわけです。

下層から更新用メソッドにアクセスできるようにする方法は、例えば

 - その更新用メソッドを、毎回Widgetのコンストラクタに渡して下層まで持っていく

という方法がありますが、これは中間層すべてのコンストラクタに記述が必要で、イマイチです。

せっかく`InheritedWidget`があるので、更新用メソッドにも下層から`O(1)`でアクセスしましょう！

今回は、公式の[List of state management approaches](https://flutter.dev/docs/development/data-and-backend/state-mgmt/options#inheritedwidget--inheritedmodel)からリンクされている

 - [Using Flutter Inherited Widgets Effectively](https://ericwindmill.com/articles/inherited_widget/)
 - [Flutter - Widget - State - Context - InheritedWidget](https://www.didierboelens.com/2018/06/widget-state-context-inheritedwidget/)

これらを参考に、一つ方法を紹介します。

## `InheritedWidget`で`State`自体を下層に流せ！

`StatefulWidget`を使う場合、プロパティも更新用メソッドも`State`が持っていることが多いです。更新時に`setState`を呼ばないといけないので、更新用メソッドも`State`が持たないといけない。

ならば、`State`自体を`InheritedWidget`で下層に流してしまえば、そのプロパティにアクセスするのも更新用メソッドにアクセスするのも簡単ですね！

その方法を簡単にまとめるとこうです。

 - `StatefulWidget`と`InheritedWidget`のペアで使う
 - `InheritedWidget`は`private`にする
 - `State`を`public`にする
 - `InheritedWidget`は`data`というプロパティを持ち、`State`を格納する
 - `of`メソッドは`StatefulWidget`が持ち、`State`(`InheritedWidget`の`data`プロパティ)を返す
 - `StatefulWidget`は`child`を受け取り、`State`の`build`で`InheritedWidget`の直下に入れて返す
 - メソッドだけ欲しい場合は`BuildContext.getElementForInheritedWidgetOfExactType`を使うと、更新伝播の対象にならない

実例でコメントと一緒に見るのが早いと思いますので、次をご覧ください。

## 使用例

ボタンが押された回数をテキストで表示するだけのアプリを考えます。

ただし、状態管理WidgetはWidgetツリーの上層にあり、ボタンとテキストはツリーの違う枝に置きます。


今回は状態更新用WidgetとアプリUI用Widgetでファイルを分けます。


### `count`を管理する`StatefulWidget`+`InheritedWidget`のセット

まず、状態管理用のWidgetを用意します。

```dart:count_manager.dart
import 'package:flutter/material.dart';

// InheritedWidgetはStateを保持するだけの役。
// privateにします。
class _StateContainer extends InheritedWidget {
  const _StateContainer({Key key, @required Widget child, this.data})
      : assert(child != null),
        super(key: key, child: child);

  // CountManagerStateのインスタンスを保持し、下層に流します。
  final CountManagerState data;

  @override
  bool updateShouldNotify(_StateContainer old) {
    return true;
  }
}

// 状態管理用Widgetです。
class CountManager extends StatefulWidget {
  const CountManager({this.child});

  final Widget child;

  // ofメソッドはStatefulWidgetが持ちます。
  // ofにlisten引数を追加します。デフォルトはtrueです。
  static CountManagerState of(BuildContext context, {bool listen = true}) =>
      listen
          ? context.dependOnInheritedWidgetOfExactType<_StateContainer>().data
          // listen==falseの場合、このcontextが更新の対象にならないようにします。
          : (context
                  .getElementForInheritedWidgetOfExactType<_StateContainer>()
                  .widget as _StateContainer)
              .data;
  @override
  CountManagerState createState() => CountManagerState();
}

class CountManagerState extends State<CountManager> {
  // 下層からアクセスしたいプロパティです。
  int count = 0;

  // 下層からアクセスしたい状態更新用メソッドです。
  void increment() {
    setState(() {
      count++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return _StateContainer(
      // InheritedWidgetのdataに、このStateインスタンス自体を渡す。
      data: this,
      child: widget.child,
    );
  }
}
```


`BuildContext`の[getElementForInheritedWidgetOfExactType](https://api.flutter.dev/flutter/widgets/BuildContext/getElementForInheritedWidgetOfExactType.html)は、`InheritedWidget`の中身にアクセスしつつも、`BuildContext`を状態更新時のリビルド対象として登録**しません**。

状態更新用メソッドだけを取得して、リビルド対象にしたくない場合、こちらを使います。

(`BuildContext`の[getElementForInheritedWidgetOfExactType](https://api.flutter.dev/flutter/widgets/BuildContext/getElementForInheritedWidgetOfExactType.html)の返り値は`InheritedWidget`ではなく[InheritedElement](https://api.flutter.dev/flutter/widgets/InheritedElement-class.html)なので、返り値の処理が若干変わってます)


### UI構築

```dart:update_method.dart
import 'package:flutter/material.dart';
import 'package:learn_inherited_widget/count_manager.dart';

class MainWidget extends StatelessWidget {
  const MainWidget();
  @override
  Widget build(BuildContext context) {
    // CountManagerの傘下に、ボタンと表示用テキストを配置します。
    return CountManager(
      child: Scaffold(
        appBar: AppBar(),
        body: Column(
          children: [
            const ButtonWidget(),
            const ShowCount(),
          ],
        ),
      ),
    );
  }
}

class ButtonWidget extends StatelessWidget {
  const ButtonWidget();
  @override
  Widget build(BuildContext context) {
    return RaisedButton(
        child: const Text('increment!!'),
        onPressed: () {
          // ofメソッドでCountManagerの状態更新用メソッドにアクセスします！
          // ボタンをリビルドする必要はないので、listen:falseとします。
          CountManager.of(context, listen: false).increment();
        });
  }
}

class ShowCount extends StatelessWidget {
  const ShowCount();
  @override
  Widget build(BuildContext context) {
    // ofメソッドでCountManagerのプロパティにアクセスします！
    // 更新を反映する必要があるので、listenはデフォルト(true)にします。
    final count = CountManager.of(context).count;
    return Text('count: $count');
  }
}
```

![Jan-05-2021 22-26-59](https://user-images.githubusercontent.com/12369062/103651668-4faf0380-4fa5-11eb-90f3-f16eb320ded1.gif)



## Providerパッケージについて

先程の例の`count_manager.dart`は50行近くあるのですが、`count`プロパティと`increment`メソッド以外は、毎回ほぼ同じ内容を書くことになります。

そこで、それをパッケージ化すると便利そうだなと思うわけなのですが、

**そうして出来上がったパッケージが[Providerパッケージ](https://pub.dev/packages/provider)です。**


(実際には、他にもいろいろな機能が追加されているので単純にそうとは言えませんが)

ということで、基本的には`Provider`パッケージを使えばいいと思います。

同じ作者によってさらなる改良がなされた[Riverpod](https://pub.dev/packages/riverpod)もありますね。

もちろん、他にもお好みの状態管理手法を使ってもいいです。

この記事で紹介した方法を使ってもいいと思います。

この記事で紹介した方法は、**Flutter以外のパッケージに一切依存しない**ところがメリットですね。


# おわり

おわりです！これで`InheritedWidget`を理解してもらえたら嬉しいです！