Flutter: カウンターアプリを InheritedWidget で実装してみる

Flutter: カウンターアプリを InheritedWidget で実装してみる

June 1, 2024

自己理解ために InheritedWidget でのデータ保持&アクセスをステップバイステップで実装していきます。

1. InheritedWidget で保持するデータに子ウィジェットからアクセスする #

count のデータを保持する InheritedWidget を定義しておき、ルートのウィジェット付近に InheritedWidget を配置します。こうすることで、InheritedWidget の配下に位置するウィジェットは .of(context)count を取得できるようになります。

import 'package:flutter/material.dart';

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

class InheritedCounterWidget extends InheritedWidget {
  const InheritedCounterWidget({
    super.key,
    required this.count,
    required super.child,
  });

  final int count;

  @override
  bool updateShouldNotify(InheritedCounterWidget oldWidget) {
    return count != oldWidget.count;
  }

  static InheritedCounterWidget of(
    BuildContext context, {
    bool listen = false,
  }) {
    final result = listen
        ? context.dependOnInheritedWidgetOfExactType<InheritedCounterWidget>()
        : context
            .getElementForInheritedWidgetOfExactType<InheritedCounterWidget>()
            ?.widget as InheritedCounterWidget?;
    assert(result != null, 'No InheritedCounterWidget found in context');
    return result!;
  }
}

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: InheritedCounterWidget(
        count: 0,
        child: Column(
          children: [
            MyText(),
          ],
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    final counter = InheritedCounterWidget.of(context);

    return Text(counter.count.toString());
  }
}

2. InheritedWidgetcount をカウントアップできるようにする #

先ほどのコードだと、count の値は定数 0 を設定しており、カウントアップすることができません。

今度は MyAppstate として _count を保持しておき、InheritedWidget でそれを配下から参照できるようにします。

これによりボタンを押すと面上の数字がカウントアップされるようになります。

なおこのとき .of(context) での参照の仕方を次のように変更する必要があります。

final counter = InheritedCounterWidget.of(context); // -> dependOnInheritedWidgetOfExactType
👇
final counter = InheritedCounterWidget.of(context, listen: true); // -> getElementForInheritedWidgetOfExactType

そうしないとカウントアップしてもそれを参照している MyText が再ビルドされないため、画面描画が更新されません。

import 'package:flutter/material.dart';

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

class InheritedCounterWidget extends InheritedWidget {
  const InheritedCounterWidget({
    super.key,
    required this.count,
    required super.child,
  });

  final int count;

  @override
  bool updateShouldNotify(InheritedCounterWidget oldWidget) {
    return count != oldWidget.count;
  }

  static InheritedCounterWidget of(
    BuildContext context, {
    bool listen = false,
  }) {
    final result = listen
        ? context.dependOnInheritedWidgetOfExactType<InheritedCounterWidget>()
        : context
            .getElementForInheritedWidgetOfExactType<InheritedCounterWidget>()
            ?.widget as InheritedCounterWidget?;
    assert(result != null, 'No InheritedCounterWidget found in context');
    return result!;
  }
}

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

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: InheritedCounterWidget(
        count: _count,
        child: Column(
          children: [
            const MyText(),
            MyButton(onPressed: () {
              setState(() {
                _count++;
              });
            }),
          ],
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    final counter = InheritedCounterWidget.of(context, listen: true);

    return Text(counter.count.toString());
  }
}

class MyButton extends StatelessWidget {
  const MyButton({
    super.key,
    required this.onPressed,
  });

  final VoidCallback onPressed;

  @override
  Widget build(BuildContext context) {
    return IconButton(
      icon: const Icon(Icons.add),
      onPressed: onPressed,
    );
  }
}

3. MyApp_count ステートを外部に移動する #

いまは MyApp_count ステートを保持していますが、今後 InheritedWidget で配下からアクセスさせたいステートが増えるにつれて、MyApp でのステートの宣言が膨れ上がっていくことが予想されます。

今後コード量が増えていったときにも管理しやすくなるように MyApp で保持している InheritedWidget 用のステートを外部に移します。リファクタリングですね。

import 'package:flutter/material.dart';

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

class InheritedCounterWidget extends InheritedWidget {
  const InheritedCounterWidget({
    super.key,
    required this.data,
    required super.child,
  });

  final CounterWidgetState data;

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

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

  final Widget child;

  @override
  State<CounterWidget> createState() => CounterWidgetState();

  static InheritedCounterWidget of(
    BuildContext context, {
    bool listen = false,
  }) {
    final result = listen
        ? context.dependOnInheritedWidgetOfExactType<InheritedCounterWidget>()
        : context
            .getElementForInheritedWidgetOfExactType<InheritedCounterWidget>()
            ?.widget as InheritedCounterWidget?;
    assert(result != null, 'No InheritedCounterWidget found in context');
    return result!;
  }
}

class CounterWidgetState extends State<CounterWidget> {
  int count = 0;

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

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

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: CounterWidget(
        child: Column(
          children: [
            MyText(),
            MyButton(),
          ],
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    final counter = CounterWidget.of(context, listen: true);

    return Text(counter.data.count.toString());
  }
}

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

  @override
  Widget build(BuildContext context) {
    final counter = CounterWidget.of(context);

    return IconButton(
      icon: const Icon(Icons.add),
      onPressed: () {
        counter.data.increment();
      },
    );
  }
}

4. もう少しだけリファクタリングする #

counter.data.countcounter.data.increment() を見たときに data を記述させるのが無駄に感じられます。.of() の戻り値を少しだけリファクタリングして、呼び出す側からより簡潔に使えるようにします。

import 'package:flutter/material.dart';

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

class InheritedCounterWidget extends InheritedWidget {
  const InheritedCounterWidget({
    super.key,
    required this.data,
    required super.child,
  });

  final CounterWidgetState data;

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

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

  final Widget child;

  @override
  State<CounterWidget> createState() => CounterWidgetState();

  static CounterWidgetState of(
    BuildContext context, {
    bool listen = false,
  }) {
    final result = listen
        ? context.dependOnInheritedWidgetOfExactType<InheritedCounterWidget>()
        : context
            .getElementForInheritedWidgetOfExactType<InheritedCounterWidget>()
            ?.widget as InheritedCounterWidget?;
    assert(result != null, 'No InheritedCounterWidget found in context');
    return result!.data;
  }
}

class CounterWidgetState extends State<CounterWidget> {
  int count = 0;

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

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

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: CounterWidget(
        child: Column(
          children: [
            MyText(),
            MyButton(),
          ],
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    final counter = CounterWidget.of(context, listen: true);

    return Text(counter.count.toString());
  }
}

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

  @override
  Widget build(BuildContext context) {
    final counter = CounterWidget.of(context);

    return IconButton(
      icon: const Icon(Icons.add),
      onPressed: () {
        counter.increment();
      },
    );
  }
}

参考 #

Dart API Document

ブログ