docs

TOP(About this memo)) > 一覧(Flutter) > 状態管理

公式ドキュメント

(参考)WidgetとElementの概要図



Ephemeral state(ローカルな状態) vs app state(共有状態)

StatelessWidget

StatefulWidget/State

リビルド時のElementの再利用

GlobalKey

(参考)親がリビルドされた際の、子のビルドについて具体例

import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';

// デバッグの出力のためのtestWidgetsラッパー
void testWidgetsAndPrint(
  String description,
  WidgetTesterCallback callback,
) {
  testWidgets(description, (widgetTester) async {
    debugPrint(">>> start first build(mount)");
    await callback(widgetTester);
    debugPrint("<<< test end");
  });
}

// デバッグの出力のためのmixin
mixin DebugMixin<T extends StatefulWidget> on State<T> {
  int mutableData = 0;

  @override
  void initState() {
    super.initState();
    debugPrint("$runtimeType.initState@$hashCode");
  }

  @override
  void didUpdateWidget(_) {
    super.didUpdateWidget(_);
    debugPrint("$runtimeType.didUpdateWidget@$hashCode");
  }

  @override
  Widget build(_) {
    debugPrint("$runtimeType.build@$hashCode mutableData:${(++mutableData)}");
    return Container();
  }

  @override
  void activate() {
    super.activate();
    debugPrint("$runtimeType.activate@$hashCode");
  }

  @override
  void deactivate() {
    super.deactivate();
    debugPrint("$runtimeType.deactivate@$hashCode");
  }

  @override
  void dispose() {
    debugPrint("$runtimeType.dispose@$hashCode");
    super.dispose();
  }
}

// 指定のStatefulWidgetのsetStateを実行して次のフレームへ
Future<void> setStateAndPump(
    WidgetTester tester, Type type, String? print) async {
  (tester.element(find.byType(type).first) as StatefulElement)
      .state
      // ignore:invalid_use_of_protected_member
      .setState(() {});
  debugPrint(">>> next frame${print ?? ""}");
  await tester.pump();
}

main() {
  testWidgetsAndPrint(
      "同じオブジェクトを子として使う: 子のbuild(), didUpdateWidget()は実行されない。子のStateのデータが維持される。",
      (tester) async {
    // ignore:prefer_const_constructors
    final w = ChildWidget();
    await tester.pumpWidget(ParentWidget(
      childBuilder: () => w,
    ));

    await setStateAndPump(tester, ParentWidget, "(ParentのsetStateを実行)");
    await setStateAndPump(tester, ChildWidget, "(ChildのsetStateを実行)");
  });

  testWidgetsAndPrint(
      "同じオブジェクト(const)を子として使う: 子のbuild(), didUpdateWidget()は実行されない。子のStateのデータが維持される。",
      (tester) async {
    await tester.pumpWidget(ParentWidget(
      childBuilder: () => const ChildWidget(),
    ));

    await setStateAndPump(tester, ParentWidget, "(ParentのsetStateを実行)");
  });

  testWidgetsAndPrint(
      "都度新しいオブジェクト(同一のruntimeType)を子として使う: 子のbuild(), didUpdateWidget()が実行される。子のStateのデータが維持される",
      (tester) async {
    await tester.pumpWidget(ParentWidget(
      // ignore:prefer_const_constructors
      childBuilder: () => ChildWidget(),
    ));

    await setStateAndPump(tester, ParentWidget, "(ParentのsetStateを実行)");
  });

  testWidgetsAndPrint(
      "階層が変わる: 新しい子のinitState(), build()が実行される。前の子のdispose()が実行され子のStateのデータは維持されない。",
      (tester) async {
    int count = 0;
    await tester.pumpWidget(ParentWidget(
      childBuilder: () => (count++) % 2 == 0
          ? const ChildWidget()
          : const Column(
              children: [ChildWidget()],
            ),
    ));

    await setStateAndPump(tester, ParentWidget, "(ParentのsetStateを実行)");
  });

  testWidgetsAndPrint(
      "階層は変わるがkeyを使って再利用する: 子のbuild(), didUpdateWidget()が実行される。子のStateのデータが維持される",
      (tester) async {
    int count = 0;
    final key = GlobalKey();
    await tester.pumpWidget(ParentWidget(
      childBuilder: () => (count++) % 2 == 0
          ? ChildWidget(key: key)
          : Column(
              children: [ChildWidget(key: key)],
            ),
    ));

    await setStateAndPump(tester, ParentWidget, "(ParentのsetStateを実行)");
  });

  testWidgetsAndPrint(
      "異なるruntimeTypeの子を使う: 新しい子のinitState(), build()が実行される。前の子のdispose()が実行され子のStateのデータは維持されない。",
      (tester) async {
    int count = 0;
    await tester.pumpWidget(ParentWidget(
      childBuilder: () =>
          (count++) % 2 == 0 ? const ChildWidget() : const ChildWidget2(),
    ));

    await setStateAndPump(tester, ParentWidget, "(ParentのsetStateを実行)");
  });
}

class ParentWidget extends StatefulWidget {
  const ParentWidget({
    super.key,
    required this.childBuilder,
  });
  final Widget Function() childBuilder;
  @override
  State<ParentWidget> createState() => ParentWidgetState();
}

class ParentWidgetState extends State<ParentWidget> with DebugMixin {
  @override
  Widget build(BuildContext context) {
    super.build(context);
    return widget.childBuilder();
  }
}

class ChildWidget extends StatefulWidget {
  const ChildWidget({super.key});
  @override
  ChildWidgetState createState() => ChildWidgetState();
}

class ChildWidgetState extends State<ChildWidget> with DebugMixin {}

class ChildWidget2 extends StatefulWidget {
  const ChildWidget2({super.key});
  @override
  ChildWidgetState2 createState() => ChildWidgetState2();
}

class ChildWidgetState2 extends State<ChildWidget2> with DebugMixin {}

State.setState()

BuildContext

import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';

// デバッグの出力のためのtestWidgetsラッパー
void testWidgetsAndPrint(
  String description,
  WidgetTesterCallback callback,
) {
  testWidgets(description, (widgetTester) async {
    debugPrint(">>> start first build(mount)");
    await callback(widgetTester);
    debugPrint("<<< test end");
  });
}

// デバッグの出力のためのmixin
mixin DebugMixin<T extends StatefulWidget> on State<T> {
  @override
  void initState() {
    debugPrint("$runtimeType.initState@$hashCode");
    dumpBuildContext(context);
    super.initState();
  }

  @override
  void didUpdateWidget(_) {
    debugPrint("$runtimeType.didUpdateWidget@$hashCode");
    dumpBuildContext(context);
    super.didUpdateWidget(_);
  }

  @override
  Widget build(_) {
    debugPrint("$runtimeType.build@$hashCode");
    dumpBuildContext(context);
    return Container();
  }

  @override
  void dispose() {
    debugPrint("$runtimeType.dispose@$hashCode");
    dumpBuildContext(context);
    super.dispose();
  }
}

// 指定のStatefulWidgetのsetStateを実行して次のフレームへ
Future<void> setStateAndPump(
    WidgetTester tester, Type type, String? print) async {
  (tester.element(find.byType(type).first) as StatefulElement)
      .state
      // ignore:invalid_use_of_protected_member
      .setState(() {});
  debugPrint(">>> next frame${print ?? ""}");
  await tester.pump();
}

main() {
  testWidgetsAndPrint("", (tester) async {
    await tester.pumpWidget(NotificationListener<MyNotification>(
      child: const MyWidget(),
      onNotification: (notification) {
        debugPrint("Notification catched: $notification");
        return true;
      },
    ));

    BuildContext e = tester.element(find.byType(MyWidget).first);
    debugPrint(e.describeElement("describeElement").value.toString());
    debugPrint(e.describeWidget("describeWidget").value.toString());
    e.dispatchNotification(MyNotification());
    debugPrint(e.findRenderObject().toString());
    debugPrint(e
        .findAncestorWidgetOfExactType<NotificationListener<MyNotification>>()
        .toString());
  });
}

class MyNotification extends Notification {}

void dumpBuildContext(BuildContext context) {
  void debugWithIndent(String str) => debugPrint("   $str");

  debugWithIndent("mounted:${context.mounted.toString()}");
  if (!context.owner!.debugBuilding && context.mounted) {
    debugWithIndent("size:${context.size.toString()}");
  }
  if (context.mounted) {
    debugWithIndent("widget:${context.widget.toString()}");
  }
  debugWithIndent("debugDoingBuild:${context.debugDoingBuild.toString()}");
}

class MyWidget extends StatefulWidget {
  const MyWidget({super.key});
  @override
  MyWidgetState createState() => MyWidgetState();
}

class MyWidgetState extends State<MyWidget> with DebugMixin {}

(参考) StatefulWidget/Stateを一つのクラスで扱う

非同期処理

import 'package:flutter/material.dart';

main() => runApp(const MaterialApp(
      home: MyWidget(),
    ));

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

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

class _MyWidgetState extends State<MyWidget> {
  List<int>? data;

  @override
  void initState() {
    super.initState();
    getDummyData();// 非同期処理の呼び出し(待たない。)
  }

  getDummyData() async {
    await Future.delayed(const Duration(milliseconds: 500), () {
      data = Iterable.generate(100, (i) => i).toList();// 非同期処理が完了したタイミングの処理
      setState(() {});
    });
  }

  @override
  Widget build(BuildContext context) {// 表示制御
    return data == null
        ? const Center(// 非同期処理が完了するまでの表示。
            child: CircularProgressIndicator(),
          )
        : ListView.builder(// 非同期処理が完了した後の表示
            itemCount: data!.length,
            itemBuilder: (_, i) => Text(data![i].toString()));
  }
}

FutureBuilder

import 'package:flutter/material.dart';

main() => runApp(const MaterialApp(
      home: MyWidget(),
    ));

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

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

class _MyWidgetState extends State<MyWidget> {
  Future<List<int>> getDummyData() async {
    await Future.delayed(const Duration(milliseconds: 500), () {});
    return Iterable.generate(100, (i) => i).toList();
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      future: getDummyData(),
      builder: (context, snapshot) {
        if (snapshot.hasData) {
          return ListView.builder(
              itemCount: snapshot.data!.length,
              itemBuilder: (_, i) => Text(snapshot.data![i].toString()));
        } else if (snapshot.hasError) {
          return const Text("error");
        } else {
          return const Center(
            child: CircularProgressIndicator(),
          );
        }
      },
    );
  }
}

StreamBuilder

InheritedWidget

具体的なユースケース

// lib/logo.dart
import 'package:flutter/material.dart';

enum LogoAspect { backgroundColor, large }

class LogoController {
  late _LogoState _logo;

  Color get color => _logo._color;

  void attach(_LogoState logo) {
    _logo = logo;
  }

  void changeColor(Color color) {
    _logo._color = color;
    _logo._setState();
  }

  void changeSize() {
    _logo._large = !_logo._large;
    _logo._setState();
  }
}

class Logo extends StatefulWidget {
  const Logo({super.key, this.initialColor = Colors.blue, this.logoController});

  final Color initialColor;
  final LogoController? logoController;

  @override
  State<Logo> createState() => _LogoState();
}

class _LogoState extends State<Logo> {
  bool _large = false;
  late Color _color = widget.initialColor;

  void _setState() => setState(() {});

  @override
  void initState() {
    super.initState();
    if (widget.logoController != null) {
      widget.logoController!.attach(this);
    }
  }

  @override
  Widget build(BuildContext context) {
    return _LogoData(
      backgroundColor: _color,
      large: _large,
      child: const _Background(
        child: _Text(),
      ),
    );
  }
}

class _LogoData extends InheritedWidget {
  const _LogoData({
    this.backgroundColor,
    this.large,
    required super.child,
  });

  final Color? backgroundColor;
  final bool? large;

  static Color? backgroundColorOf(BuildContext context) {
    return context
        .dependOnInheritedWidgetOfExactType<_LogoData>()
        ?.backgroundColor;
  }

  static bool? sizeOf(BuildContext context) {
    return context
        .dependOnInheritedWidgetOfExactType<_LogoData>()
        ?.large;
  }

  @override
  bool updateShouldNotify(_LogoData oldWidget) {
    return backgroundColor != oldWidget.backgroundColor ||
        large != oldWidget.large;
  }
}

class _Background extends StatelessWidget {
  const _Background({required this.child});

  final Widget child;

  @override
  Widget build(BuildContext context) {
    final Color color =
        _LogoData.backgroundColorOf(context) ?? Colors.white;
    debugPrint("BackgroundWidget build");

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

class _Text extends StatelessWidget {
  const _Text();

  @override
  Widget build(BuildContext context) {
    debugPrint("LogoWidget build");
    final bool largeLogo = _LogoData.sizeOf(context) ?? true;

    return Icon(
      Icons.abc,
      size: largeLogo ? 200.0 : 100.0,
    );
  }
}
// lib/main.dart
import 'package:flutter/material.dart';
import './logo.dart';

void main() {
  runApp(MaterialApp(
    home: MyWidget(),
  ));
}

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

  final LogoController controller = LogoController();

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: [
        Center(
          child: Logo(
            initialColor: Colors.blue,
            logoController: controller,
          ),
        ),
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            TextButton(
                onPressed: () => controller.changeColor(
                    controller.color == Colors.blue ? Colors.red : Colors.blue),
                child: const Text('Update background color')),
            TextButton(
                onPressed: () => controller.changeSize(),
                child: const Text('Resize Logo'))
          ],
        )
      ],
    );
  }
}

InheritedModel

(参考)InheritedNotifier

Listenable

abstract class Listenable {
  const Listenable();
  //...
  void addListener(VoidCallback listener);
  void removeListener(VoidCallback listener);
}
import 'package:flutter/material.dart';
main () {
  final notifier = MyNotifier();
  notifier.addListener(()=> print("notified: ${notifier.data}"));
  notifier.updateData(3);
  // notified:3
}

class MyNotifier with ChangeNotifier {
  int data = 0;
  
  void updateData(int data) {
    this.data = data;
    notifyListeners();
  }
}

ListenableBuilder

main() {
  testWidgets("", (tester) async {
    final notifier = MyNotifier();

    dump() => debugPrint(
        (tester.element(find.byType(MyWidget).first) as StatelessElement)
            .toStringDeep());

    await tester.pumpWidget(MaterialApp(
      home: MyWidget(myNotifier: notifier),
    ));

    dump();
    notifier.updateData(3);
    await tester.pump();
    dump();
  });
}

class MyNotifier with ChangeNotifier {
  int data = 0;

  void updateData(int data) {
    this.data = data;
    notifyListeners();
  }
}

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

  final MyNotifier myNotifier;

  @override
  Widget build(BuildContext context) {
    return ListenableBuilder(
      listenable: myNotifier,
      builder: (context, child) => Text(myNotifier.data.toString()),
    );
  }
}

/*

MyWidget
└ListenableBuilder(listenable: Instance of 'MyNotifier', state: _AnimatedState#36861)
 └Text("0", dependencies: [DefaultSelectionStyle, DefaultTextStyle, MediaQuery])
  └RichText(softWrap: wrapping at box width, maxLines: unlimited, text: "0", dependencies: [Directionality, _LocalizationsScope-[GlobalKey#dbb61]], renderObject: RenderParagraph#d5d13)

MyWidget
└ListenableBuilder(listenable: Instance of 'MyNotifier', state: _AnimatedState#36861)
 └Text("3", dependencies: [DefaultSelectionStyle, DefaultTextStyle, MediaQuery])
  └RichText(softWrap: wrapping at box width, maxLines: unlimited, text: "3", dependencies: [Directionality, _LocalizationsScope-[GlobalKey#dbb61]], renderObject: RenderParagraph#d5d13)

*/

ValueNotifier

void main() {
  test('valueNotifier test', () async {
    final notifier = ValueNotifier<int>(1);
    var isListenerCalled = false;
    notifier.addListener(() {
      isListenerCalled = true;
    });
    notifier.value = notifier.value;
    expect(isListenerCalled, false);// 値が変わらなければリスナーはコールされない。
    notifier.value += 1;
    expect(isListenerCalled, true);
  });
}

(IMO) アプリ全体で利用するデータは、Listenable派生クラスとInheritedWidget派生クラスのどちらを利用するか?