LoginSignup
30

More than 3 years have passed since last update.

InheritedWidgetの目的と使い方【Flutter】

Last updated at Posted at 2021-01-05

まえがき

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

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

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

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

前提知識

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

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

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

Flutterのcontextの「お気持ち」を理解する

記事内の例の実行方法

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

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

InheritedWidgetの存在理由

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

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

一つずつ説明します。

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

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

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

ランダウの記号#無限大における漸近挙動と計算量の見積り - Wikipedia

dependOnInheritedWidgetOfExactTypeを使え!

BuildContextにはdependOnInheritedWidgetOfExactTypeというメソッドがあります。

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

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

基本的にfindAncestorStateOfTypeは使うな!

BuildContextにはfindAncestorStateOfTypeというメソッドもあり、同じようにBuildContextからWidgetツリーをさかのぼって一番近くにある、指定した型のStateを返すのですが、その時の計算時間はO(n)です。

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

使用例

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

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

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

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

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

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

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

Theme, MediaQuery, Directionalityなど多くのFlutter Widgetがこの性質を持っていますね。実はそこでもInheritedWidgetが利用されています。

次の例を見てください。

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

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

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

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

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

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

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

答えはこうです。

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

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

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

使用例

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

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

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

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

updateShouldNotifyで更新を間引きする

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

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

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

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

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

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

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

Jan-03-2021 20-40-54

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

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

ここまで、動きの理解を重視して、BuildContext.dependOnInheritedWidgetOfExactTypeを直接呼んでいましたが、

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

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

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を、アプリの状態管理にガッツリ使うことを考えます。

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

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

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

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

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

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

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

今回は、公式のList of state management approachesからリンクされている

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

InheritedWidgetState自体を下層に流せ!

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

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

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

  • StatefulWidgetInheritedWidgetのペアで使う
  • InheritedWidgetprivateにする
  • Statepublicにする
  • InheritedWidgetdataというプロパティを持ち、Stateを格納する
  • ofメソッドはStatefulWidgetが持ち、State(InheritedWidgetdataプロパティ)を返す
  • StatefulWidgetchildを受け取り、StatebuildInheritedWidgetの直下に入れて返す
  • メソッドだけ欲しい場合はBuildContext.getElementForInheritedWidgetOfExactTypeを使うと、更新伝播の対象にならない

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

使用例

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

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

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

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

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

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

BuildContextgetElementForInheritedWidgetOfExactTypeは、InheritedWidgetの中身にアクセスしつつも、BuildContextを状態更新時のリビルド対象として登録しません

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

(BuildContextgetElementForInheritedWidgetOfExactTypeの返り値はInheritedWidgetではなくInheritedElementなので、返り値の処理が若干変わってます)

UI構築

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

Providerパッケージについて

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

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

そうして出来上がったパッケージがProviderパッケージです。

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

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

同じ作者によってさらなる改良がなされたRiverpodもありますね。

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

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

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

おわり

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30