Flutter: 複数の InheritedWidget を参照しているときの build() の実行数

Flutter: 複数の InheritedWidget を参照しているときの build() の実行数

June 2, 2024

Flutter で複数の InheritedWidget の値を参照しているときに、参照先ウィジェットがどのタイミングで再ビルドされるのかを確認したかった。その記録。

2つの InheritedWidget の値が同時に更新される場合 #

結果:build() は1回だけ実行される。

import 'package:flutter/material.dart';

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

// -------------------
// Inherited Counter A
// -------------------
class InheritedCounterA extends InheritedWidget {
  const InheritedCounterA({
    super.key,
    required this.data,
    required super.child,
  });

  final CounterStateA data;

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

class CounterA extends StatefulWidget {
  const CounterA({
    super.key,
    required this.child,
  });

  final Widget child;

  @override
  State<CounterA> createState() => CounterStateA();

  static CounterStateA of(BuildContext context) {
    final result =
        context.dependOnInheritedWidgetOfExactType<InheritedCounterA>();
    assert(result != null);
    return result!.data;
  }
}

class CounterStateA extends State<CounterA> {
  int count = 0;

  void increment() {
    setState(() {
      count++;
    });
  }

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

// -------------------
// Inherited Counter B
// -------------------
class InheritedCounterB extends InheritedWidget {
  const InheritedCounterB({
    super.key,
    required this.data,
    required super.child,
  });

  final CounterStateB data;

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

class CounterB extends StatefulWidget {
  const CounterB({
    super.key,
    required this.child,
  });

  final Widget child;

  @override
  State<CounterB> createState() => CounterStateB();

  static CounterStateB of(BuildContext context) {
    final result =
        context.dependOnInheritedWidgetOfExactType<InheritedCounterB>();
    assert(result != null);
    return result!.data;
  }
}

class CounterStateB extends State<CounterB> {
  int count = 0;

  void increment() {
    setState(() {
      count++;
    });
  }

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

// ----------
// UI Widgets
// ----------
class MyApp extends StatelessWidget {
  const MyApp({
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: CounterA(
        child: CounterB(
          child: MyWidget(),
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    final counterA = CounterA.of(context);
    final counterB = CounterB.of(context);

    print("Re-build");
    print("CounterA count: ${counterA.count}");
    print("CounterB count: ${counterB.count}");

    return Column(
      children: [
        Text("CounterA count: ${counterA.count}"),
        Text("CounterB count: ${counterB.count}"),
        IconButton(
          icon: const Icon(Icons.add),
          onPressed: () {
            counterA.increment();
            counterB.increment();
          },
        ),
      ],
    );
  }
}
# 初回描画
Re-build
CounterA count: 0
CounterB count: 0
# カウントアップボタン押下1回目
Re-build
CounterA count: 1
CounterB count: 1
# カウントアップボタン押下2回目
Re-build
CounterA count: 2
CounterB count: 2
# カウントアップボタン押下3回目
Re-build
CounterA count: 3
CounterB count: 3

2つの InheritedWidget の値が時間差で更新される場合 #

結果:build() は2回実行される。InheritedWidget A に対して1回、InheritedWidget B に対して1回。

import 'package:flutter/material.dart';

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

// -------------------
// Inherited Counter A
// -------------------
class InheritedCounterA extends InheritedWidget {
  const InheritedCounterA({
    super.key,
    required this.data,
    required super.child,
  });

  final CounterStateA data;

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

class CounterA extends StatefulWidget {
  const CounterA({
    super.key,
    required this.child,
  });

  final Widget child;

  @override
  State<CounterA> createState() => CounterStateA();

  static CounterStateA of(BuildContext context) {
    final result =
        context.dependOnInheritedWidgetOfExactType<InheritedCounterA>();
    assert(result != null);
    return result!.data;
  }
}

class CounterStateA extends State<CounterA> {
  int count = 0;

  void increment() {
    Future.delayed(const Duration(seconds: 1), () {
      setState(() {
        count++;
      });
    });
  }

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

// -------------------
// Inherited Counter B
// -------------------
class InheritedCounterB extends InheritedWidget {
  const InheritedCounterB({
    super.key,
    required this.data,
    required super.child,
  });

  final CounterStateB data;

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

class CounterB extends StatefulWidget {
  const CounterB({
    super.key,
    required this.child,
  });

  final Widget child;

  @override
  State<CounterB> createState() => CounterStateB();

  static CounterStateB of(BuildContext context) {
    final result =
        context.dependOnInheritedWidgetOfExactType<InheritedCounterB>();
    assert(result != null);
    return result!.data;
  }
}

class CounterStateB extends State<CounterB> {
  int count = 0;

  void increment() {
    Future.delayed(const Duration(seconds: 2), () {
      setState(() {
        count++;
      });
    });
  }

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

// ----------
// UI Widgets
// ----------
class MyApp extends StatelessWidget {
  const MyApp({
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: CounterA(
        child: CounterB(
          child: MyWidget(),
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    final counterA = CounterA.of(context);
    final counterB = CounterB.of(context);

    print("Re-build");
    print("CounterA count: ${counterA.count}");
    print("CounterB count: ${counterB.count}");

    return Column(
      children: [
        Text("CounterA count: ${counterA.count}"),
        Text("CounterB count: ${counterB.count}"),
        IconButton(
          icon: const Icon(Icons.add),
          onPressed: () {
            counterA.increment();
            counterB.increment();
          },
        ),
      ],
    );
  }
}
# 初回描画
Re-build
CounterA count: 0
CounterB count: 0
# カウントアップボタン押下1回目
# (1秒後)
Re-build
CounterA count: 1
CounterB count: 0
# (2秒後)
Re-build
CounterA count: 1
CounterB count: 1
# カウントアップボタン押下2回目
# (1秒後)
Re-build
CounterA count: 2
CounterB count: 1
# (2秒後)
Re-build
CounterA count: 2
CounterB count: 2
# カウントアップボタン押下3回目
# (1秒後)
Re-build
CounterA count: 3
CounterB count: 2
# (2秒後)
Re-build
CounterA count: 3
CounterB count: 3

親ウィジェットでキャッシュしている場合 #

結果:親ウィジェットでキャッシュしていても、子ウィジェットが InheritedWidget を参照していれば子ウィジェットの build() は実行される。

import 'package:flutter/material.dart';

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

// -------------------
// Inherited Counter A
// -------------------
class InheritedCounterA extends InheritedWidget {
  const InheritedCounterA({
    super.key,
    required this.data,
    required super.child,
  });

  final CounterStateA data;

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

class CounterA extends StatefulWidget {
  const CounterA({
    super.key,
    required this.child,
  });

  final Widget child;

  @override
  State<CounterA> createState() => CounterStateA();

  static CounterStateA of(BuildContext context) {
    final result =
        context.dependOnInheritedWidgetOfExactType<InheritedCounterA>();
    assert(result != null);
    return result!.data;
  }
}

class CounterStateA extends State<CounterA> {
  int count = 0;

  void increment() {
    Future.delayed(const Duration(seconds: 1), () {
      setState(() {
        count++;
      });
    });
  }

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

// -------------------
// Inherited Counter B
// -------------------
class InheritedCounterB extends InheritedWidget {
  const InheritedCounterB({
    super.key,
    required this.data,
    required super.child,
  });

  final CounterStateB data;

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

class CounterB extends StatefulWidget {
  const CounterB({
    super.key,
    required this.child,
  });

  final Widget child;

  @override
  State<CounterB> createState() => CounterStateB();

  static CounterStateB of(BuildContext context) {
    final result =
        context.dependOnInheritedWidgetOfExactType<InheritedCounterB>();
    assert(result != null);
    return result!.data;
  }
}

class CounterStateB extends State<CounterB> {
  int count = 0;

  void increment() {
    Future.delayed(const Duration(seconds: 2), () {
      setState(() {
        count++;
      });
    });
  }

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

// ----------
// UI Widgets
// ----------
class MyApp extends StatelessWidget {
  const MyApp({
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: CounterA(
        child: CounterB(
          child: MyWidget(),
        ),
      ),
    );
  }
}

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

  @override
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  Widget? _cachedWidget;
  (int countA, int countB)? _previousCount;

  Widget _build(BuildContext context) {
    final counterA = CounterA.of(context);
    final counterB = CounterB.of(context);

    return Column(
      children: [
        Text("CounterA count: ${counterA.count}"),
        Text("CounterB count: ${counterB.count}"),
        IconButton(
          icon: const Icon(Icons.add),
          onPressed: () {
            counterA.increment();
            counterB.increment();
          },
        ),
      ],
    );
  }

  @override
  Widget build(BuildContext context) {
    final counterA = CounterA.of(context);
    final counterB = CounterB.of(context);

    print("Re-build");
    print("CounterA count: ${counterA.count}");
    print("CounterB count: ${counterB.count}");

    return _cachedWidget = _cachedWidget == null ||
            _previousCount != (counterA.count, counterB.count)
        ? _build(context)
        : _cachedWidget!;
  }
}
# 初回描画
Re-build
CounterA count: 0
CounterB count: 0
# カウントアップボタン押下1回目
# (1秒後)
Re-build
CounterA count: 1
CounterB count: 0
# (2秒後)
Re-build
CounterA count: 1
CounterB count: 1
# カウントアップボタン押下2回目
# (1秒後)
Re-build
CounterA count: 2
CounterB count: 1
# (2秒後)
Re-build
CounterA count: 2
CounterB count: 2
# カウントアップボタン押下3回目
# (1秒後)
Re-build
CounterA count: 3
CounterB count: 2
# (2秒後)
Re-build
CounterA count: 3
CounterB count: 3