0
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

たぶん理解できたFlutterのInheritedWidget

Posted at

:book: Flutterの記事を整理し本にしました :book:

  • 本稿の記事を含む様々な記事を体系的に整理し本にまとめました
  • 今後はこちらを最新化するため、最新情報はこちらをご確認くださ
  • 10万文字を超える超大作になっています(笑)

はじめに

  • いままで何となくにしていたInheritedWidgetを学びなおしたので、整理して記事にしてみました。

まとめ

InheritedWidget

これまで、StatelessWidgetStatefulWidgetが登場してきましたが、InheritedWidgetというものもあります。

Flutterでは、Widgetを入れ子構造で構築していくため、ツリーがどんどん深くなり、祖先や子孫をたどるのに時間がかかる傾向があります。
また、ツリーの根に近い部分から再構築する場合のコストも高くなります。

そこで、素早く祖先をたどったり、再構築を部分ツリーに限定することができれば処理効率が良くなります。

特徴1:O(1)でのWidgetやフィールドへのアクセス

Widgetが祖先のWidgetの変数にアクセスしたい場合、上位から順次パラメタ引数で渡していくのが一般的です

pic1.png

しかし、階層が深くなっていくと、すべてをパラメタで渡すのはプログラムが煩雑になり、素通しする中間のWidgetがでてきます。

また、仮にすべてのWidgetにグローバルキーを設定しておけば、このキーを使って直接アクセスができ、O(1)を実現できますが、全てにグローバルキーを付けるのも管理が大変になります。

そこで、祖先のWidgetやそのフィールドへ素早くアクセスできる手段があると効率的で、これを提供するのが、InheritedWidgetです。

pic2.png

InheritedWidgetは下位のツリーからO($1$)でアクセスできるようになっています。

Oはランダウ記号と呼ばれ、処理にかかる計算量の支配項を表すのに使われます。
O($1$) : 直接アクセス。ハッシュなど
O($n$) : データ数に比例。ループ処理の線型検索など
O($n^2$) : データの2乗に比例。二重ループ処理のバブルソートなど
O($log_2n$) : 要素を半分にしながら行う処理。二分探索など
O($nlog_2n$) : O($n$)とO($log_2n$)を組み合わせて行うような処理。ヒープソート。マージソートなど

今回もHelloWorldをサンプルに、動作を確認していきます。

main.dart
import 'package:flutter/material.dart';
import 'package:hello_world/Widgets.dart';
import 'MyInheritedWidget.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, this.title}) : super(key: key);
  final String? title;

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

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    // InheritedWidgetが間に挟まり、childでScaffoldを指定している
    return MyInheritedWidget( 
        message: "I am InheritedWidget",
        child: Scaffold(
          appBar: AppBar(
            title: Text(widget.title!),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                // Container->Center->Row->Column->WidgetAの階層で呼び出す
                // 階層を深くしたいだけなので、Container~Columnまでに意味はなし
                Container(
                    child: Center(
                        child: Row(
                            mainAxisAlignment: MainAxisAlignment.center,
                            children: [
                      Column(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: [WidgetA()])
                    ]))),
              ],
            ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: _incrementCounter,
            tooltip: 'Increment',
            child: Icon(Icons.add),
          ), // This trailing comma makes auto-formatting nicer for build methods.
        ));
  }
}

まず、通常Scaffoldを返すbuild関数に、MyInheritedWidgetを間に挟んでいます。

次に、結果を出力するWidgetAの上位にContainer->Center->Row->Columnを入れています。これは、階層を深くしても直接アクセスができることを説明したいだけの実質ダミーのWidgetです。大切なのは末端のWidgetAです。

最後に、このWidgetAMyInheritedWidgetのもつmessageにアクセスします。

不要部分を減らしたいので、下記の部分は今回は削除しています。
Text('You have pushed the button this many times:')
Text('$_counter',style: Theme.of(context).textTheme.headline4,)

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

class MyInheritedWidget extends InheritedWidget {
  final String message;
  // コンストラクタでメッセージと子Widgetを取る
  MyInheritedWidget({required this.message, required Widget child})
      : super(child: child);

  // O(1)でInheritedWidgetを返却
  static MyInheritedWidget of(BuildContext context) =>
      context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>()
          as MyInheritedWidget;

  //更新されたかどうかの判定ロジック
  @override
  bool updateShouldNotify(MyInheritedWidget oldWidget) =>
      oldWidget.message != message;
}

こちらがMyInheritedWidgetの定義です。
messageと下位となるWidgetツリーを引数で受け取るコンストラをもっています。
今回はchildにはScaffoldが入ってきます。

また、staticメソッドのofをオーバライドしています。
ofメソッドはcontextを使って、ウィジェットツリーをさかのぼって探すメソッドです。
dependOnInheritedWidgetOfExactTypeがO($1$)で処理できます。

ofメソッドについては、よくわからないという方は、BuildContextとofメソッドをご参照ください

Widgets.dart
import 'package:flutter/material.dart';
import 'package:hello_world/MyInheritedWidget.dart';
class WidgetA extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    MyInheritedWidget _myInheritedWidget = MyInheritedWidget.of(context);
    String message = _myInheritedWidget.message;
    return Text(message);
  }
}

最後が末端で利用されるWidgetAの実体です。
buildの中で、messageを含むTextを返していますが、そのmessageMyInheritedwidgetから持ってきています。

まず、ofメソッドでインスタンスを取得し、その後messageフィールドから値を取り出しています。

上記の例は単なる文字列ですが、serviceオブジェクトなどを共有できるようにしておき、取得後サービスのメソッドを呼び出して活用することもできます。

特徴2:下位ツリーの部分ビルド

Flutterでは、Widgetツリーの再構築や頻繁に行われています。

そこで、以下のような2つのニーズがあります。

  • あえて作り直しを無効化させて作り直さない
  • 狙い撃ちで部分ツリーを強制的に再構築する

値を変えない例

まず、再構築の動作例を見る前に、値が変わらない例を作っておきます。
Stringmessageではなく、intcountを利用します。
Scaffoldの下のCenterを静的な定義にして作り変わらないようにします。

main.dart
void main() {/* 変わらないので省略 */ }
class MyApp extends StatelessWidget { /* 変わらないので省略 */ }
class MyHomePage extends StatefulWidget { /* 変わらないので省略 */ }
class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
    print("count:" + _counter.toString());
  }

  // Scaffoldの下のCenter部分を先に静的に作っておき、作り返さないように制御
  // 深い階層の伝播は証明できたためにシンプルにCenter->WidgetAに変更
  final Widget _widget = Center(child: WidgetA());
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title!),
      ),
      // 静的に作ったCenterより下のツリーを配置する
      body: _widget,
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

上記のように、事前に作っておき、Widgetが変わらないように制御すると、カウントは増えますが、画面は切り替わりません

ほぼ重複なので省略しますが、WidgetAとMyInheritedWidgetのString messageをint counterに変更していますので、ご注意ください。

result.sh
I/flutter (23418): count:1
I/flutter (23418): count:2
I/flutter (23418): count:3

pic3.png

値を変える例

次に、InheritedWidgetの動作を確認するために、bodyを下記のようにして、MyInheritedWidgetを挟んでみます。

main.dart
//ビルドメソッド以外は同じのため省略
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title!),
      ),
+      body: MyInheritedWidget(count: _counter, child: _widget),
-      //body: _widget,
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

pic4.png

InheritedWidgetのフィールドを介して、状態の伝播が伝わり、Widgetが作り直されています。
では、どうやって変更されたかを認識できるのでしょうか?
その正体が、updateShouldNotifyです。

MyInheritedWidget.dart
//ビルドメソッド以外は同じのため省略
import 'package:flutter/material.dart';
class MyInheritedWidget extends InheritedWidget {
  final int count;
  MyInheritedWidget({required this.count, required Widget child})
      : super(child: child);
  // O(1)でInheritedWidgetを返却
  static MyInheritedWidget of(BuildContext context) =>
      context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>()
          as MyInheritedWidget;
  //更新されたかどうかの判定ロジック
  @override
  bool updateShouldNotify(MyInheritedWidget oldWidget) {
-    //return oldWidget.count != count;
+    return (oldWidget.count != count && count % 2 == 0);
  }
}

このupdateShouldNotifyメソッドの戻り値がtrue/falseで更新するかどうかを判断します。
パラメタのoldWidgetで前の状態のWidgetを受け取り、前と今の差分を比較できるようになっています。
ためしに、countが前回と異なるという条件に加えて、2の倍数の時だけtrueになるように変更してみます。

すると、下記のように、2の倍数の時だけ画面描画が行われるようになります。
pic5.png

更新ロジックを整理すると、下記のようになります。
pic6.png

0
3
0

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
0
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?