skip to Main Content

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


  1. 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

    Update:

    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 a
    ValueListenable.
    Given a ValueListenable<T> and a builder which builds widgets from
    concrete values of T, this class will automatically register itself as
    a listener of the ValueListenable 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!

    Login or Signup to reply.
  2. 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search