InheritedWidget/InheritedModelとは何か

# 概要

flutterでのWidgetは大きく分けて、4つあります。

* StatelessWidget
* StatefullWidget
* InheritedWidget
* RenderObjectWidget

です。

StatelessWidgetとStatefullWidgetはサンプルでよく見かけます。
RenderObjectWidgetとは簡単に言えば、レンダリングするWidgetでほとんどのWidgetがこれになります。

InheritedWidgetは、おそらく初級向けのサンプルや記事にはでてこないのではないでしょうか。また、公式ドキュメントには具体的な実装方法が書いていません。

この記事ではInheritedWidgetのできることとサンプルを提供したいと思います。

その前にBuildContextについて理解していないと難しいので、[こちらの記事](https://qiita.com/ko2ic/items/f7bf98b4a30049027470)を先に閲覧することをお勧めします。

# 何ができるのか

まずは、[ドキュメント](https://docs.flutter.io/flutter/widgets/InheritedWidget-class.html)を確認してみましょう。

>Base class for widgets that efficiently propagate information down the tree.

>To obtain the nearest instance of a particular type of inherited widget from a build context, use BuildContext.inheritFromWidgetOfExactType.

>Inherited widgets, when referenced in this way, will cause the consumer to rebuild when the inherited widget itself changes state.

>訳)
ツリーの下に情報を効率的に伝達するウィジェットの基本クラス。

>ビルドコンテキストから継承されたウィジェットの特定の型の最も近いインスタンスを取得するには、```BuildContext.inheritFromWidgetOfExactType```を使用します。

>継承されたウィジェットは、このように参照されると、継承されたウィジェット自体が状態を変更したときにコンシューマを再構築させます。

普通のWidgetは、ツリー構造の下のWidgetが先祖の変更を感知して、自身をリビルドする or しないの判断はできません。
しかし、InheritedWidgetを利用すれば、それができるのです。

![スクリーンショット 2019-01-24 17.02.12.png](https://qiita-image-store.s3.amazonaws.com/0/10494/f5863e5f-e69a-47ff-e46f-dacdf7ca786f.png)

すごく簡単な図ですが、Cボタンを押下したら、Stateが変更されて、Aのテキストだけがリビルドされて表示が変わる。
こういったことができます。

余談ですが、InheritedWidgetの記事の説明で、「子孫から親のStateにアクセスを可能にする」的な記述が書かれたりしていますが、それはInheritedWidgetの説明としては完全では**ない**です。そもそも、それはInheritedWidgetを使わなくともBuildContxtで簡単に実現可能です。（それは後ほど記述します）
**重要なのは子孫が親の変更を感知できて、buildメソッドを再度呼ぶことで再構築し直すことができる**ことと**先祖をO(1)で取得できる**ことです。

# サンプルアプリの仕様

プラスボタン(=WidgetC)を押下すると_HomePageStateの_counterが増えて上部のテキスト(=WidgetA)が変更されます。
真ん中の「I am a widget that will not be rebuilt.」=(WidgetB)は変更されません。

![new.png](https://qiita-image-store.s3.amazonaws.com/0/10494/bbb82e94-2dcd-365c-11f5-10bb9d216456.png)



# InheritedWidgetを使わない場合

まずは、使わない場合のソースです。
この場合、ボタンを押下するたびに、WidgetA,WidgetB,WidgetC共にbuildメソッドが呼ばれます。
これは説明不要だと思います。

```main.dart
class TopPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: Text('Demo'),
        ),
        body: HomePage(),
      ),
    );
  }
}

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          WidgetA(_counter),
          WidgetB(),
          WidgetC(_incrementCounter),
        ],
      ),
    );
  }
}

class WidgetA extends StatelessWidget {
  final int counter;

  WidgetA(this.counter);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text(
        '${counter}',
        style: Theme.of(context).textTheme.display1,
      ),
    );
  }
}

class WidgetB extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text('I am a widget that will not be rebuilt.');
  }
}

class WidgetC extends StatelessWidget {
  final void Function() incrementCounter;

  WidgetC(this.incrementCounter);

  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      onPressed: () {
        incrementCounter();
      },
      child: Icon(Icons.add),
    );
  }
}
```

AndroidStudioのFlutter Performanceを見ると実際にWidgetA,WidgetB,WidgetCがリビルドされているのがわかります。
![old.gif](https://qiita-image-store.s3.amazonaws.com/0/10494/8e6c5cd0-d7f5-4b0f-da38-6425feb20b5a.gif)

# InheritedWidgetを使う場合

まずは全体のソースを載せます。

```main.dart
class TopPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: HomePage(
        child: Scaffold(
          appBar: AppBar(
            title: Text('InheritedWidget Demo'),
          ),
          body: Column(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: <Widget>[
              WidgetA(),
              WidgetB(),
              WidgetC(),
            ],
          ),
        ),
      ),
    );
  }
}

class _MyInheritedWidget extends InheritedWidget {
  _MyInheritedWidget({
    Key key,
    @required Widget child,
    @required this.data,
  }) : super(key: key, child: child);

  final HomePageState data;

  @override
  bool updateShouldNotify(_MyInheritedWidget oldWidget) {
    return true;
  }
}

class HomePage extends StatefulWidget {
  HomePage({
    Key key,
    this.child,
  }) : super(key: key);

  final Widget child;

  @override
  HomePageState createState() => HomePageState();

  static HomePageState of(BuildContext context, {bool rebuild = true}) {
    if (rebuild) {
      return (context.inheritFromWidgetOfExactType(_MyInheritedWidget) as _MyInheritedWidget).data;
    }
    return (context.ancestorWidgetOfExactType(_MyInheritedWidget) as _MyInheritedWidget).data;
    // 実は下を使うの方が良い
    // return (context.ancestorInheritedElementForWidgetOfExactType(_MyInheritedWidget).widget as _MyInheritedWidget).data;
  }
}

class HomePageState extends State<HomePage> {
  int counter = 0;

  void _incrementCounter() {
    setState(() {
      counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return _MyInheritedWidget(
      data: this,
      child: widget.child,
    );
  }
}

class WidgetA extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final HomePageState state = HomePage.of(context);

    return Center(
      child: Text(
        '${state.counter}',
        style: Theme.of(context).textTheme.display1,
      ),
    );
  }
}

class WidgetB extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text('I am a widget that will not be rebuilt.');
  }
}

class WidgetC extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final HomePageState state = HomePage.of(context, rebuild: false);
    return RaisedButton(
      onPressed: () {
        state._incrementCounter();
      },
      child: Icon(Icons.add),
    );
  }
}
```

WidgetBとWidgetCはリビルドされていないのがわかります。

![new.gif](https://qiita-image-store.s3.amazonaws.com/0/10494/f1a970b0-e522-8873-3fa4-33066ac2e075.gif)

## 説明

InheritedWidgetを使うことで変わった部分をみていきましょう

### WidgetA,WidgetC

WidgetBは変わりません。

WidgetA、WidgetCともにStateの取得方法が変わっています。
旧バージョンはコンストラクタで_counter変数とincrementCounter関数を受け取っていました。

```dart
final HomePageState state = HomePage.of(context); // WidgetAの場合
final HomePageState state = HomePage.of(context, rebuild: false);  // WidgetCの場合
```

新バージョンでは、HomePageStateを取得しています。
WidgetCの場合は、rebuildを明示的にfalseにしています。WidgetC=ボタンは変化しないのでrebuildをさせないことを知らせています。
WidgetCは、Stateのメソッドを呼び出したいのでStateに依存しているだけです。

では、HomePageのofメソッドをみてみましょう。

### HomePage

```dart
  static HomePageState of(BuildContext context, {bool rebuild = true}) {
    if (rebuild) {
      return (context.inheritFromWidgetOfExactType(_MyInheritedWidget) as _MyInheritedWidget).data;
    }
    return (context.ancestorWidgetOfExactType(_MyInheritedWidget) as _MyInheritedWidget).data;
    // 実は下を使うの方が良い
    // return (context.ancestorInheritedElementForWidgetOfExactType(_MyInheritedWidget).widget as _MyInheritedWidget).data;
  }
```

このメソッドの処理は先祖を辿って ```_MyInheritedWidget``` を探しています。 
```_MyInheritedWidget``` に ```HomePageState``` を持たせることでState情報を子供Widgetに渡せるようにしています。

どうして先祖が取得できるのかは、BuildContextの仕組みがわかれば簡単にわかります。[こちらの記事](https://qiita.com/ko2ic/items/f7bf98b4a30049027470)を参照してください。

```inheritFromWidgetOfExactType``` と ```ancestorWidgetOfExactType``` の違いは、呼び出したcontextが保持するwidgetがリビルドされるかされないかです。
また```inheritFromWidgetOfExactType``` は ```InheritedWidget```　の取得専用のメソッドです。
これが呼ばれるとリビルドされます。
```ancestorWidgetOfExactType``` は、指定したWidgetをcontextから辿って取得するだけのメソッドです。
コメントで記述している ```ancestorInheritedElementForWidgetOfExactType``` は、inheritFromWidgetOfExactTypeと同様にO(1)のコストで取得できます。

こういうわけで、WidgetA=(Text)はリビルドされて、WidgetC=(Button)はリビルドされないのです。

では、旧バージョンになかった ```_MyInheritedWidget``` はどういう実装なのか見てましょう。

### _MyInheritedWidget

```dart
class _MyInheritedWidget extends InheritedWidget {
  _MyInheritedWidget({
    Key key,
    @required Widget child,
    @required this.data,
  }) : super(key: key, child: child);

  final HomePageState data;

  @override
  bool updateShouldNotify(_MyInheritedWidget oldWidget) {
    return true;
  }
}
```

ここのポイントは、```InheritedWidget``` を継承していることです。
```HomePageState.of``` で探しているのはこのクラスです。

```updateShouldNotify``` メソッドは、```inheritFromWidgetOfExactType``` を呼び出すWidgetにリビルドをすることを通知するかをboolで返します。

子孫が利用したいのは、```HomePageState``` なのでdataフィールドで保持しています。

###  HomePageState

```dart

  @override
  Widget build(BuildContext context) {
    return _MyInheritedWidget(
      data: this,
      child: widget.child,
    );
  }
```

ここでのポイントは、```_MyInheritedWidget``` とchildで親が指定したwidgetを指定していることです。

旧バージョンは、ここでWidegtA,WidgetB,WidgetCがありました。それは変更される（setStateされた時にbuildする）WidgetなのでStateのbuildでインスタンスしなければなりませんでした。

新バージョンでは、InheritedWidgetを使ってビルドを伝えるのでここでインスタンスしてはいけません。
なので、親が定義する方になっています。

### TopPage

で、実際にWidegtA,WidgetB,WidgetCをインスタンスしているのは、```TopPage```　になります。

```dart
class TopPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    ・・・
          body: Column(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: <Widget>[　
              WidgetA(), // それぞれのWidgetのインスタンス化はこのクラスに移動
              WidgetB(),
              WidgetC(),
            ],
          ),
    ・・・
  }
}
```

# InheritedModelについて

InheritedWidgetを継承している[InheritedModel](https://docs.flutter.io/flutter/widgets/InheritedModel-class.html)というのがあります。

これは、InheritedWidgetの変更を購読している子孫Widgetの中で、一部のWidgetだけに変更を伝えたい場合に使えます。
単純に文字列のキーを送って、それに該当する子孫だけに変更を伝えます。

上記サンプルの```final HomePageState state = HomePage.of(context, rebuild: false);``` で、rebuildするかどうかを送っているのと同じことです。

違う部分だけ記述しておきます。

```diff
   @override
   HomePageState createState() => HomePageState();

-  static HomePageState of(BuildContext context, {bool rebuild = true}) {
-    if (rebuild) {
-      return (context.inheritFromWidgetOfExactType(_MyInheritedWidget) as _MyInheritedWidget).data;
-    }
-    return (context.ancestorWidgetOfExactType(_MyInheritedWidget) as _MyInheritedWidget).data;
+  static HomePageState of(BuildContext context, String aspect) {
+    return InheritedModel.inheritFrom<_MyInheritedWidget>(context, aspect: aspect).data;
   }
 }
```

```diff
-class _MyInheritedWidget extends InheritedWidget {
・・・
+class _MyInheritedWidget extends InheritedModel {

+  @override
+  bool updateShouldNotifyDependent(_MyInheritedWidget old, Set aspects) {
+    return aspects.contains('A'); // A文字列が送られてきた場合だけ通知する
+  }
 }
```

```diff
 class WidgetA extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
-    final HomePageState state = HomePage.of(context);
+    final HomePageState state = HomePage.of(context, 'A'); // A文字列なのでリビルドする
```

```diff
 class WidgetC extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
-    final HomePageState state = HomePage.of(context, rebuild: false);
+    final HomePageState state = HomePage.of(context, 'C'); // C文字列なのでリビルドされない
```

# Builder Widgetできめ細かいリビルド

```inheritFromWidgetOfExactType``` を呼び出すbuild内で一部のWidgetだけ再構築させたくない場合があります。わざわざ別クラスに切り出すほどの大きさじゃない場合です。

以下のコードで、Text("AAAAA")はリビルドさせたくない。

```dart
class WidgetA extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final HomePageState state = HomePage.of(context);

    return Column(
      children: <Widget>[
        Center(
          child: Text(
            '${state.counter}',
            style: Theme.of(context).textTheme.display1,
          ),
        ),
        Text("AAAAA"), // ここはリビルドさせたくない
      ],
    );
  }
}
```

BuildContextとInheritedWidgetの仕組みがわかっていれば、簡単なことです。

Builder Widgetを使って、対象のcontextを小さくします。そのcontextで ```inheritFromWidgetOfExactType``` を呼び出せば、そのWidgetだけがリビルドされます。

```dart
    return Column(
      children: <Widget>[
        Center(
          child: Builder(builder: (context){
            final HomePageState state = HomePage.of(context);
            return Text(
              '${state.counter}',
              style: Theme.of(context).textTheme.display1,
            );
          }),
        ),
        Text("AAAAA"),
      ],
    );
```

buildメソッドの引数のBuildContextのwidgetは、WidgetAですが、Builder内のBuildContextのwidgetは、Builderなので、Builder内だけがリビルドされるという仕組みです。

![new.gif](https://qiita-image-store.s3.amazonaws.com/0/10494/c6fcfb08-84a5-3d1e-34b1-2f432963ce14.gif)

# その他

### 子孫が親のStateを取得するだけならInheritedWidgetじゃなくても可能

最初に「子孫から親のStateにアクセスを可能にする」は、InheritedWidgetの説明としては間違い。と記述しました。
それだけだったら他の方法でできるからです。その実装を載せておきます。

```dart
class TopPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: Text('Demo'),
        ),
        body: HomePage(),
      ),
    );
  }
}

class HomePage extends StatefulWidget {
  HomePageState state; // Stateを保持する

  @override
  HomePageState createState() {
    state = HomePageState();
    return state;
  }
}

class HomePageState extends State<HomePage> {
  int counter = 0; // privateを辞める

  void incrementCounter() { // privateを辞める
    setState(() {
      counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          WidgetA(),
          WidgetB(),
          WidgetC(),
        ],
      ),
    );
  }
}

class WidgetA extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final HomePage widget = context.ancestorWidgetOfExactType(HomePage); // これでStateを取得
    final HomePageState state = widget?.state;

    return Center(
      child: Text(
        '${state == null ? 0 : state.counter}',
        style: Theme.of(context).textTheme.display1,
      ),
    );
  }
}

class WidgetB extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text('I am a widget that will not be rebuilt.');
  }
}

class WidgetC extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final HomePage widget = context.ancestorWidgetOfExactType(HomePage);
    final HomePageState state = widget?.state;

    return RaisedButton(
      onPressed: () {
        state?.incrementCounter();
      },
      child: Icon(Icons.add),
    );
  }
}
```

単純に ```ancestorWidgetOfExactType``` メソッドを使って、StatefullWidgetのStateを使っているだけです。
ただし、この方法では、親の変更を子孫が感知できないので、同じ値のままです。
また、このメソッドはO(N)のコストがかかってしまいます。

なので、InheritedWidgetを使うのです。

# 最後に

flutter界隈でよく使われるデザインパターンで、Scoped ModelとBLoC(Business Logic of component)があります。
これらのパターンの実装は、InheritedWidgetを利用しています。
また、flutter自体の実装でもInheritedWidgetは多用されています。

初級を抜け出すには、まずは、InheritedWidgetから始めると良いかと思います。

[ソースはこちら](https://github.com/ko2ic/spike_inherited_widget)