TOP(About this memo)) > 一覧(Flutter) > 状態管理
In other words, there is no need to use state management techniques (ScopedModel, Redux, etc.) on this kind of state. All you need is a StatefulWidget.
For managing app state, you’ll want to research your options. Your choice depends on the complexity and nature of your app, your team’s previous experience, and many other aspects. Read on.
To be clear, you can use State and setState() to manage all of the state in your app. In fact, the Flutter team does this in many simple app samples (including the starter app that you get with every flutter create).
When asked about React’s setState versus Redux’s store, the author of Redux, Dan Abramov, replied: “The rule of thumb is: Do whatever is less awkward.” https://github.com/reduxjs/redux/issues/1287#issuecomment-175351978
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 {}
@override
Widget build(BuildContext context) {
return TextButton(
child: const Text("button"),
onPressed: () async {
await Future.delayed(const Duration(milliseconds: 100));
ScaffoldMessenger.of(context).showSnackBar(
// Don't use 'BuildContext's across async gaps.
const SnackBar(
content: Text("s"),
));
});
}
if (mounted) ScaffoldMessenger.〜〜
のように mountedが真の場合のみ実行するようにすれば良い。bool get mounted => _element != null;
@protected
void setState(VoidCallback fn) {
assert(() {
if (_debugLifecycleState == _StateLifecycle.defunct) {
// FlutterError
}
if (_debugLifecycleState == _StateLifecycle.created && !mounted) {
// FlutterError
}
//...
}());
//...
_element!.markNeedsBuild();
}
A handle to the location of a widget in the widget tree.
print((context as Element).findAncestorWidgetOfExactType<MaterialApp>());
print((context).findAncestorWidgetOfExactType<CupertinoApp>());
@override
void initState() {
super.initState();
debugPrint(Material.of(context).toString());
}
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 {}
It is however easily doable to reverse the lifecycles between State and StatefulWidget
のreverse the lifecycles
の意味するところは “ライフサイクルを逆にする”というより”ライフサイクルメソッドをStatefulWidgetの方で指定する” というニュアンスで書いているものと考えられる。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()));
}
}
@override
void initState() async {// asyncをつけるとassertエラーとなる。
super.initState();
await getDummyData();
setState((){});// なお、初回(マウント)は必ずbuild()が実行されるため、initState()内のsetState()は意味がない。
}
getDummyData() async {
await Future.delayed(const Duration(milliseconds: 500), () {
data = Iterable.generate(100, (i) => i).toList();
});
}
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(),
);
}
},
);
}
}
複数の子孫ウィジェットで共通で使うデータを持たせたい・監視させたい場合
例として、_Backgroundウィジェットと_Textウィジェットで構成されるLogoウィジェットを考えてみる。_Backgroundと_Textだけが利用したい共通のイミュータブルなデータがあるとする。
構成としては以下の図のようなものが考えられる。
ソースコードは下記のようになる。
// 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'))
],
)
],
);
}
}
上記のコードは以下を参考にしている。
その他
The listeners are typically used to notify clients that the object has been updated.
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();
}
}
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)
*/
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);
});
}