🔖

【Flutter Widget of the Week #15】InheritedModelを使ってみた

2022/10/22に公開

はじめに

Flutter Widget of the Week #15 InheritedModel についてまとめましたので、紹介します。
https://youtu.be/ml5uefGgkaA

InheritedModel とは

Flutter で使われている Widget は大きく分けると、

  1. StatelessWidget
  2. StatefullWidget
  3. InheritedWidget
  4. RenderObjectWidget

の4つがあります。
そのうちの一つである InheritedWidget。
この InheritedWidgetを使えば、深くネストされた子孫の widget から 親 widget(widget ツリーの上位にある widget)の保存データを直接参照することができます。
しかし、InheritedWidgetの場合、データを変更すると、全ての子孫 widget が再構築されてしまいます。
そのため、変更が不要な widget まで再構築してしまい、余計なコストがかかってしまいます。
変更が必要な widget だけを再構築するようにしたい。
そんなときに 今回紹介する InheritedModel が使えるのです。
InheritedModelを使えば、子孫 widget 一つ一つに対して再構築が必要かどうか判定を行い、必要な widget だけ再構築することができるようになります。

では、実際のコードとともに InheritedModel の使い方を説明していきます。

InheritedModel サンプルアプリ

公式ドキュメントにあるサンプルを参考に InheritedModel の使い方を見ていきましょう。
実行したときの動きとしては以下のようになってます。
実行画面
実行画面
InheritedModel を使うには
まず InheritedWidget を継承したサブクラスを作る必要があります。
サブクラスを作ったら、子孫から参照するデータのフィールドを追加します。

// InheritedWidget を継承したサブクラス
class LogoModel extends InheritedModel<LogoAspect> {
  const LogoModel({
    super.key,
    this.backgroundColor,
    this.large,
    required super.child,
  });

  // 子孫から参照するデータのフィールド
  final Color? backgroundColor;
  final bool? large;
}

次に updateShouldNotify メソッドと updateShouldNotifyDependent メソッドをオーバーライドします。

updateShouldNotify は InheritedWidget に変更があったときに、
データを参照している widget を再構築するかどうかを判定するためのメソッドです。
この updateShouldNotify が true のとき、参照している全ての子孫が再構築されます。

class LogoModel extends InheritedModel<LogoAspect> {
  // ...
  
  // updateShouldNotify をオーバーライド
  
  bool updateShouldNotify(LogoModel oldWidget) {
    return backgroundColor != oldWidget.backgroundColor ||
        large != oldWidget.large;
  }
  
  // ...
}

updateShouldNotify のイメージ
updateShouldNotify のイメージ
ただこの場合、変更が不要な子孫 widget(右の青色の丸)まで再構築されてしまいます。
再構築する子孫 widget が少なければ問題ありませんが、多い場合、再構築にかかるコストや時間も大きくなってしまいます。こうした無駄な再構築をなくし、変更が必要な子孫 widget だけを再構築するメソッドが次の updateShouldNotifyDependent です。

updateShouldNotifyDependent は親 widget に変更があったときに、子孫 widget 一つ一つに対して1回ずつ実行され、その widget を再構築するかどうかを判定するためのメソッドです。
サンプルコードだと、アスペクトで backgroundColor を指定した子孫 widget の場合、backgroundColor の変更があったとき と アスペクトで large を指定した子孫 widget の場合、large の変更があったとき true を返し、それ以外の場合は false を返します。
※アスペクトについてはこの後説明します。

class LogoModel extends InheritedModel<LogoAspect> {
  // ...

  // updateShouldNotifyDependent をオーバーライド
  
  bool updateShouldNotifyDependent(
      LogoModel oldWidget, Set<LogoAspect> dependencies) {
    if (backgroundColor != oldWidget.backgroundColor &&
        dependencies.contains(LogoAspect.backgroundColor)) {
      return true;
    }
    if (large != oldWidget.large && dependencies.contains(LogoAspect.large)){
      return true;
    }
    return false;
  }
}

updateShouldNotifyDependent のイメージ
updateShouldNotifyDependent のイメージ
このように変更が必要な子孫 widget だけを再構築してくれます。

ここで出てくるアスペクトは 子孫 widget が親 widget を取得する際に、任意のキーワードとして指定するもので、このキーワードをもとに再構築するかどうかを判断しています。
これは、static メソッドで InheritedModel.inheritFrom を使うことで InheritedModel への依存関係を作成します。
このメソッドの context パラメータは InheritedModel が変更されたときに再構築されるサブツリーを定義します。通常、inheritFrom メソッドは InheritedModel 固有の静的メソッドから呼び出されます。
サンプルコードでは以下のようになります。

// LogoAspect を enum で設定
enum LogoAspect { backgroundColor, large }

class LogoModel extends InheritedModel<LogoAspect> {
  // ...

  // LogoAspect の backgroundColor アスペクトが変更されたときのみ context が再構築される
  static Color? backgroundColorOf(BuildContext context) {
    return InheritedModel.inheritFrom<LogoModel>(context,
            aspect: LogoAspect.backgroundColor)
        ?.backgroundColor;
  }

  // LogoAspect の large アスペクトが変更されたときのみ large が再構築される
  static bool sizeOf(BuildContext context) {
    return InheritedModel.inheritFrom<LogoModel>(context,
                aspect: LogoAspect.large)
            ?.large ??
        false;
  }
  
  // ...
}

以上が InheritedModel を使ったサブクラスの作成に必要な基本的なコードです。
これらを用いて画面やその他の機能を作った最終的なサンプルコードが以下です。

main.dart
import 'package:flutter/material.dart';

enum LogoAspect { backgroundColor, large }

class InheritedModelApp extends StatelessWidget {
  const InheritedModelApp({super.key});

  
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: InheritedModelExample(),
    );
  }
}

class LogoModel extends InheritedModel<LogoAspect> {
  const LogoModel({
    super.key,
    this.backgroundColor,
    this.large,
    required super.child,
  });

  final Color? backgroundColor;
  final bool? large;

  static Color? backgroundColorOf(BuildContext context) {
    return InheritedModel.inheritFrom<LogoModel>(context,
            aspect: LogoAspect.backgroundColor)
        ?.backgroundColor;
  }

  static bool sizeOf(BuildContext context) {
    return InheritedModel.inheritFrom<LogoModel>(context,
                aspect: LogoAspect.large)
            ?.large ??
        false;
  }

  
  bool updateShouldNotify(LogoModel oldWidget) {
    return backgroundColor != oldWidget.backgroundColor ||
        large != oldWidget.large;
  }

  
  bool updateShouldNotifyDependent(
      LogoModel oldWidget, Set<LogoAspect> dependencies) {
    if (backgroundColor != oldWidget.backgroundColor &&
        dependencies.contains(LogoAspect.backgroundColor)) {
      return true;
    }
    if (large != oldWidget.large && dependencies.contains(LogoAspect.large)) {
      return true;
    }
    return false;
  }
}

class InheritedModelExample extends StatefulWidget {
  const InheritedModelExample({super.key});

  
  State<InheritedModelExample> createState() => _InheritedModelExampleState();
}

class _InheritedModelExampleState extends State<InheritedModelExample> {
  bool large = false;
  Color color = Colors.blue;

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('InheritedModel Sample')),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: <Widget>[
          Center(
            child: LogoModel(
              backgroundColor: color,
              large: large,
              child: const BackgroundWidget(
                child: LogoWidget(),
              ),
            ),
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: <Widget>[
              ElevatedButton(
                onPressed: () {
                  ScaffoldMessenger.of(context).showSnackBar(
                    const SnackBar(
                      content: Text('Rebuilt Background'),
                      duration: Duration(milliseconds: 500),
                    ),
                  );
                  setState(() {
                    if (color == Colors.blue) {
                      color = Colors.red;
                    } else {
                      color = Colors.blue;
                    }
                  });
                },
                child: const Text('Update background'),
              ),
              ElevatedButton(
                onPressed: () {
                  ScaffoldMessenger.of(context).showSnackBar(
                    const SnackBar(
                      content: Text('Rebuilt LogoWidget'),
                      duration: Duration(milliseconds: 500),
                    ),
                  );
                  setState(() {
                    large = !large;
                  });
                },
                child: const Text('Resize Logo'),
              ),
            ],
          )
        ],
      ),
    );
  }
}

class BackgroundWidget extends StatelessWidget {
  const BackgroundWidget({super.key, required this.child});

  final Widget child;

  
  Widget build(BuildContext context) {
    final Color color = LogoModel.backgroundColorOf(context)!;

    return AnimatedContainer(
      padding: const EdgeInsets.all(12.0),
      color: color,
      duration: const Duration(seconds: 2),
      curve: Curves.fastOutSlowIn,
      child: child,
    );
  }
}

class LogoWidget extends StatelessWidget {
  const LogoWidget({super.key});

  
  Widget build(BuildContext context) {
    final bool largeLogo = LogoModel.sizeOf(context);

    return AnimatedContainer(
      padding: const EdgeInsets.all(20.0),
      duration: const Duration(seconds: 2),
      curve: Curves.fastLinearToSlowEaseIn,
      alignment: Alignment.center,
      child: FlutterLogo(size: largeLogo ? 200.0 : 100.0),
    );
  }
}

最後に

今回は InheritedModel を紹介しました。
これまでの Flutter Widget of the Week で紹介した widget とは一風変わった回でしたね。私自身初めて聞いた widget でもありました。必要な widget だけが再構築されるようにする方法は Provider や Riverpod などで状態管理を行う方法で実装するのが定番かと思っていたのですが、InheritedModel を使ってみるのも面白いやり方だなと感じました。
次は #16 ClipRRect です。またお会いしましょう。

参考記事

https://api.flutter.dev/flutter/widgets/InheritedModel-class.html
https://bukiyo-papa.com/inheritedmodel/
https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html

Discussion