I’m getting some problem with state update:
My ImpostazioniLaunchComponentCopy
is:
import 'package:flutter/material.dart';
import 'package:jeep_controller/core/enums/enums.dart';
import 'package:jeep_controller/features/shared/presentation/widgets/action_buttons/menu_action_button.dart';
import 'package:jeep_controller/features/shared/presentation/widgets/action_buttons/switch_action_button.dart';
import 'package:jeep_controller/core/constants/constants.dart';
import 'package:jeep_controller/features/shared/presentation/pages/commands_page/commands_page.dart';
class ImpostazioniLaunchComponentCopy extends StatefulWidget {
const ImpostazioniLaunchComponentCopy({
super.key,
});
@override
State<ImpostazioniLaunchComponentCopy> createState() =>
_ImpostazioniLaunchComponentCopyState();
}
class _ImpostazioniLaunchComponentCopyState
extends State<ImpostazioniLaunchComponentCopy> {
bool test = false;
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return MenuActionButton(
icon: JeepControllerIcon.impostazioni,
pageTitle: Labels.impostazioni,
onClick: () {
Navigator.of(context).push(
MaterialPageRoute(
settings: const RouteSettings(
name: Routes.commandPage,
),
builder: (context) {
return CommandsPage(
headerBarType: HeaderBarType.goBackAndLock,
title: Labels.impostazioni,
actions: getChildren(),
);
},
),
);
},
);
}
List<Widget> getChildren() {
return [
Text("test is $test"),
SwitchActionButton(
icon: null,
label: "test",
initialState: test,
onClick: (bool newValue) {
setState(() {
print("setState2: newValue = $newValue");
test = newValue;
});
},
),
];
}
}
and my CommandPage
is:
import 'package:flutter/material.dart';
import 'package:jeep_controller/core/enums/enums.dart';
import 'package:jeep_controller/features/shared/presentation/pages/base_commands_page/base_ui_commands_page.dart';
class CommandsPage extends StatefulWidget {
final HeaderBarType headerBarType;
final Function? headerBarRender;
final String title;
final List<Widget> actions;
final bool containsLaunchers;
final VoidCallback? onInit;
const CommandsPage({
super.key,
required this.headerBarType,
required this.title,
required this.actions,
this.containsLaunchers = true,
this.headerBarRender,
this.onInit,
});
@override
State<CommandsPage> createState() => _CommandsPageState();
}
class _CommandsPageState extends State<CommandsPage> {
@override
void initState() {
super.initState();
if (widget.onInit != null) {
widget.onInit!();
print("called CommandPage onInit param (${widget.title}))");
}
}
@override
Widget build(BuildContext context) {
print("builder of CommandsPage (${widget.title})");
return BaseUICommandsPage(
headerBarType: widget.headerBarType,
headerBarRender: widget.headerBarRender,
myWidget: Expanded(
child: getChild(),
),
title: widget.title,
);
}
Widget getChild() {
if (widget.containsLaunchers) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Wrap(
spacing: 15,
runSpacing: 15,
children: List.from(
widget.actions.map((w) => w),
),
),
],
);
} else {
return widget.actions[0];
}
}
}
So, in the final UI there are a label ("Test is false") and a button.
I was expecting that label changed to "Test is true" when I hitted the button but it doesn’t. It’s always "Test is false". I see the corrent log in the console with changed state:
I/flutter ( 9657): setState2: newValue = true
I/flutter ( 9657): setState2: newValue = false
I/flutter ( 9657): setState2: newValue = true
Edited to add DartPad snippet:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: Column(
children: [Impostazioni(),],
),
),
),
);
}
}
class Impostazioni extends StatefulWidget {
const Impostazioni({
super.key,
});
@override
State<Impostazioni> createState() => _ImpostazioniState();
}
class _ImpostazioniState extends State<Impostazioni> {
// bool test = false;
final ValueNotifier<bool> test = ValueNotifier<bool>(false);
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return BaseActionButton(
icon: null,
text: "impostazioni",
onClick: () {
Navigator.of(context).push(
MaterialPageRoute(
settings: const RouteSettings(
name: "/commands",
),
builder: (context) {
return CommandsPage(
title: "impostazioni",
actions: getChildren(),
);
},
),
);
},
actionButtonType: ActionButtonType.menu,
);
}
List<Widget> getChildren() {
return [
Text("test is ${test.value}"),
BaseActionButton(
icon: null,
text: "test",
initialState: test.value,
onClick: (bool newValue) {
setState(() {
print("setState2: newValue = $newValue");
test.value = newValue;
});
},
actionButtonType: ActionButtonType.stateSwitch,
),
];
}
}
class CommandsPage extends StatefulWidget {
final Function? headerBarRender;
final String title;
final List<Widget> actions;
final bool containsLaunchers;
final VoidCallback? onInit;
const CommandsPage({
super.key,
required this.title,
required this.actions,
this.containsLaunchers = true,
this.headerBarRender,
this.onInit,
});
@override
State<CommandsPage> createState() => _CommandsPageState();
}
class _CommandsPageState extends State<CommandsPage> {
@override
void initState() {
super.initState();
if (widget.onInit != null) {
widget.onInit!();
print("called CommandPage onInit param (${widget.title}))");
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Expanded(
child: getChild(),
),
],
),
);
}
Widget getChild() {
if (widget.containsLaunchers) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Wrap(
spacing: 15,
runSpacing: 15,
children: List.from(
widget.actions.map((w) => w),
),
),
],
);
} else {
return widget.actions[0];
}
}
}
enum ActionButtonType { button, stateSwitch, menu }
class BaseActionButton extends StatefulWidget {
final String? icon;
final String text;
final Function? onClick;
final bool? initialState;
final ActionButtonType actionButtonType;
final IconData? iconData;
const BaseActionButton({
super.key,
required this.icon,
required this.text,
this.onClick,
this.initialState,
required this.actionButtonType,
this.iconData,
});
@override
State<BaseActionButton> createState() => _BaseActionButtonState();
}
class _BaseActionButtonState extends State<BaseActionButton> {
bool _buttonState = false;
@override
void initState() {
super.initState();
if (widget.initialState != null) {
_buttonState = widget.initialState!;
}
}
void onClickFunction() {
bool newState = !_buttonState;
setState(() {
_buttonState = newState;
});
isSwitch ? widget.onClick?.call(newState) : widget.onClick?.call();
}
bool get isSwitch => widget.actionButtonType == ActionButtonType.stateSwitch;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(0),
child: ElevatedButton(
onPressed: onClickFunction,
style: ElevatedButton.styleFrom(
elevation: 12.0,
backgroundColor: _buttonState && isSwitch
? Colors.purple.shade100
: Colors.purple.shade200,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
),
child: Padding(
padding: const EdgeInsets.all(5.0),
child: Container(
color: Colors.transparent,
height: boxSize,
width: boxSize,
child: Column(
children: [
Container(
color: Colors.transparent,
width: boxSize / 2,
height: boxSize / 2,
child: widget.icon != null
? Image.asset(
"assets/images/${widget.icon}",
fit: BoxFit.fitWidth,
)
: (widget.iconData != null
? Icon(widget.iconData)
: const SizedBox(
width: 0,
)),
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
isMenu ? widget.text.toUpperCase() : widget.text,
maxLines: 2,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.black87,
fontSize: fontSize,
fontWeight: FontWeight.bold,
),
),
],
),
),
if (isMenu)
Text(
"[ menu ]",
style: TextStyle(
fontSize: boxSize / 10,
fontWeight: FontWeight.w600,
letterSpacing: 1,
),
),
if (widget.actionButtonType ==
ActionButtonType.stateSwitch) ...[
Container(
width: boxSize / 1.3333,
height: boxSize / 16.6666,
color: (_buttonState ? Colors.green : Colors.red),
),
],
],
),
),
),
),
);
}
double get boxSize => 106;
double get fontSize => 12;
bool get isMenu => widget.actionButtonType == ActionButtonType.menu;
}
2
Answers
Replace this:
with:
Then replace your:
with:
To use
ValueNotifier
in accordance with its core functionalities, here are the resources: ValueNotifier class and Flutter ValueNotifier with ExamplesUpdate:
You can use ValueListenableBuilder.
It is very useful to ensure that the value will update properly no matter where you want to place your data inside your widget tree, but of course, you need to use it based on your use case. Just read ValueNotifier class.
Now, to directly address your issue:
Here’s the summary to implement ValueListenableBuilder:
The initialization of your variable remains the same… so, let’s skip on that.
Wrap your
Text("test is $test")
with ValueListenableBuilder class instance like this:Then, in your:
You don’t have to wrap your variable test with
setState
to update your UI because,So… do this instead:
It should work as expected now.
I hope it helps!
Problem
The state update (setState) affects the parent widget (_ImpostazioniLaunchComponentCopyState), but since CommandsPage and its children are recreated every time, the state change doesn’t persist across navigation.
Solution
To preserve the state across navigation, you can:
Pass the updated state (test) explicitly to the CommandsPage.
Use a state management solution like Provider, GetX, or InheritedWidget.
Refactor your CommandsPage to maintain its own state or receive the initial value from the parent widget.