Reputation: 395
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
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;
}
Upvotes: 1
Views: 65
Reputation: 1
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.
Upvotes: 0
Reputation: 719
Replace this:
bool test = false;
with:
final ValueNotifier<bool> test = ValueNotifier<bool>(false);
Then replace your:
setState(() {
print("setState2: newValue = $newValue");
test = newValue;
});
with:
setState(() {
test.value = newValue;
});
To use ValueNotifier
in accordance with its core functionalities, here are the resources: ValueNotifier class and Flutter ValueNotifier with Examples
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:
ValueListenableBuilder<bool>(
valueListenable: test,
builder: (context, value, child) {
return Text('Test is $value'); // replace your bool test variable with value, because it will pass the updated value and rebuild the UI for you
}
),
Then, in your:
SwitchActionButton(
icon: null,
label: "test",
initialState: test,
onClick: (bool newValue) {
setState(() {
print("setState2: newValue = $newValue");
test = newValue;
});
},
),
You don't have to wrap your variable test with setState
to update your UI because,
ValueListenableBuilder
whose content stays synced with aValueListenable
. Given aValueListenable<T>
and a builder which builds widgets from concrete values of T, this class will automatically register itself as a listener of theValueListenable
and call the builder with updated values when the value changes.
So... do this instead:
SwitchActionButton(
icon: null,
label: "test",
initialState: test,
onClick: (bool newValue) {
test.value = newValue;
},
),
It should work as expected now.
I hope it helps!
Upvotes: 1