'Error: LateInitializedState: flutter is showing state is not initialized error after switching themeMode

I have an app and I built some helper widgets to be able to reuse. so I created this Button Widget which can show a loading icon if pressed and the async request is not completed. This is working fine when the app is hot restarted but when I am changing anything in the app or switching theme it is throwing an error that says this:

LateInitializationError: Field 'btnState' has not been initialized

However, it is working fine when creating this same concept using GetX package for state management for the busy and disabled state of the Button Widget but it is not working when using the flutter native way.

I think when switching theme GetX re-initialize all the widgets in the widget tree but StatefulWidget does not initialize it.

If you want to see GetX approach you can see here Flutter MVC Button Widget

Here is my Button Widget code

import 'package:flutter/material.dart';

import '../helpers/ColorPalette.dart';
import '../helpers/TextStyl.dart';
import 'LoadingIcon.dart';

class Button extends StatefulWidget {
  late final _ButtonState btnState;

  final String label;
  final void Function(_ButtonState)? onTap;
  final bool outline;
  final Widget? leading;
  final Widget? loadingIcon;
  final bool block;
  final Color? backgroundColor;
  final Color? color;
  final bool flat;

  Button({
    Key? key,
    required this.label,
    this.onTap,
    this.leading,
    this.loadingIcon,
    this.flat = false,
    this.backgroundColor = kcPrimary,
    this.color,
  })  : outline = false,
        block = false,
        super(key: key);

  @override
  State<Button> createState() {
    btnState = _ButtonState();
    return btnState;
  }
}

class _ButtonState extends State<Button> {
  bool isBusy = false;
  bool isDisabled = false;

  setBusy(bool val) {
    setState(() {
      isBusy = val;
    });
    return widget.btnState;
  }

  setDisabled(bool val) {
    setState(() {
      isDisabled = val;
    });
    return widget.btnState;
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        if (!isBusy && !isDisabled) {
          widget.onTap!(widget.btnState);
        }
      },
      child: widget.block
          ? AnimatedContainer(
              duration: const Duration(milliseconds: 250),
              padding: EdgeInsets.symmetric(vertical: 12.0, horizontal: 20.0),
              width: double.infinity,
              alignment: Alignment.center,
              decoration: !widget.outline
                  ? BoxDecoration(
                      color: !isDisabled ? widget.backgroundColor : widget.backgroundColor?.withOpacity(0.5),
                      borderRadius: BorderRadius.circular(!widget.flat ? 8 : 0),
                    )
                  : BoxDecoration(
                      color: Colors.transparent,
                      borderRadius: BorderRadius.circular(!widget.flat ? 8 : 0),
                      border: Border.all(
                        color: !isDisabled ? widget.backgroundColor! : widget.backgroundColor!.withOpacity(0.5),
                        width: 1,
                      ),
                    ),
              child: !isBusy
                  ? Row(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        if (widget.leading != null) widget.leading!,
                        if (widget.leading != null) SizedBox(width: 5),
                        Text(
                          widget.label,
                          style: TextStyl.button(context)?.copyWith(
                            fontWeight: !widget.outline ? FontWeight.bold : FontWeight.w400,
                            color: !widget.outline
                                ? widget.color != null
                                    ? widget.color
                                    : getContrastColor(widget.backgroundColor!)
                                : widget.backgroundColor,
                          ),
                        ),
                      ],
                    )
                  : widget.loadingIcon != null
                      ? SizedBox(height: 20, width: 20, child: widget.loadingIcon)
                      : LoadingIcon(
                          color: !widget.outline
                              ? widget.color != null
                                  ? widget.color
                                  : getContrastColor(widget.backgroundColor!)
                              : widget.backgroundColor,
                          height: 16,
                        ),
            )
          : Row(
              crossAxisAlignment: CrossAxisAlignment.center,
              mainAxisSize: MainAxisSize.min,
              children: [
                AnimatedContainer(
                  duration: const Duration(milliseconds: 250),
                  padding: EdgeInsets.symmetric(vertical: 12.0, horizontal: 20.0),
                  alignment: Alignment.center,
                  decoration: !widget.outline
                      ? BoxDecoration(
                          color: !isDisabled ? widget.backgroundColor! : widget.backgroundColor!.withOpacity(0.5),
                          borderRadius: BorderRadius.circular(!widget.flat ? 8 : 0),
                          border: Border.all(
                            color: widget.backgroundColor!,
                            width: 1,
                          ),
                        )
                      : BoxDecoration(
                          color: Colors.transparent,
                          borderRadius: BorderRadius.circular(!widget.flat ? 8 : 0),
                          border: Border.all(
                            color: widget.backgroundColor!,
                            width: 1,
                          ),
                        ),
                  child: !isBusy
                      ? Row(
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            if (widget.leading != null) widget.leading!,
                            if (widget.leading != null) SizedBox(width: 5),
                            Text(
                              widget.label,
                              style: TextStyl.button(context)?.copyWith(
                                fontWeight: !widget.outline ? FontWeight.bold : FontWeight.w400,
                                color: !widget.outline
                                    ? widget.color != null
                                        ? widget.color
                                        : getContrastColor(widget.backgroundColor!)
                                    : widget.backgroundColor!,
                              ),
                            ),
                          ],
                        )
                      : widget.loadingIcon != null
                          ? SizedBox(height: 20, width: 20, child: widget.loadingIcon)
                          : LoadingIcon(
                              color: !widget.outline
                                  ? widget.color != null
                                      ? widget.color
                                      : getContrastColor(widget.backgroundColor!)
                                  : widget.backgroundColor!,
                              height: 16,
                            ),
                ),
              ],
            ),
    );
  }
}

And here is the code where I used the Button Widget:

Button(
  key: UniqueKey(),
  label: "Test",
  onTap: (btn) async {
    btn.setBusy(true);
    await Future.delayed(2.seconds);
    btn.setBusy(false);
  },
),


Solution 1:[1]

If you find yourself trying to manually access/manipulate the private _State class of any StatefulWidget then its a sign that you need to find a better approach to whatever it is you're trying to do.

I would avoid stuff like this

 late final _ButtonState btnState; // this

...

 @override
  State<Button> createState() {
    btnState = _ButtonState(); // this
    return btnState;
  }

...

  final void Function(_ButtonState)? onTap; // using private state as arguments


If you want to do what that repo is doing with GetX in a native Flutter way then here's one way to approach it using ValueNotifier

If everything in the button depends on the status of an isBusy and isDisabled bool, then I'd create a custom class that ValueNotifier will sync to.

class ButtonStatus {
  ButtonStatus({required this.isBusy, required this.isDisabled});

  final bool isBusy;
  final bool isDisabled;
}

Then a static controller class with relevant methods with similar functionality to the Getx class in the repo in addition to the same thing you're currently passing in as a test.

class ButtonStatusController {
// status is what ValueNotifier listens to
  static ValueNotifier<ButtonStatus> status = ValueNotifier(
    ButtonStatus(isBusy: false, isDisabled: false),
  );

  // equivalent to the function you're passing in the button
  static Future<void> testFunction() async {
    final isDisabled = status.value.isDisabled;
    status.value = ButtonStatus(isBusy: true, isDisabled: isDisabled);
    await Future.delayed(Duration(seconds: 2));
    status.value = ButtonStatus(isBusy: false, isDisabled: isDisabled);
  }

  // updating ButtonStatus
  static void updateButtonStatus(
      {required bool isBusy, required bool isDisabled}) {
    status.value = ButtonStatus(isBusy: isBusy, isDisabled: isDisabled);
  }
}

Then your Button gets wrapped in a ValueListenableBuilder<ButtonStatus> and can be stateless. It will rebuild based on changes to the ButtonStatus and the functionality won't be affected by any changes to the Theme of the app.

class Button extends StatelessWidget {
  final String label;
  final void Function() onTap;
  final bool outline;
  final Widget? leading;
  final Widget? loadingIcon;
  final bool block;
  final Color? backgroundColor;
  final Color? color;
  final bool flat;

  Button({
    Key? key,
    required this.label,
    required this.onTap,
    this.leading,
    this.loadingIcon,
    this.flat = false,
    this.backgroundColor = kcPrimary,
    this.color,
  })  : outline = false,
        block = false,
        super(key: key);

  @override
  Widget build(BuildContext context) {
    // wrapping in ValueListenableBuilder of type ButtonStatus
    return ValueListenableBuilder<ButtonStatus>(
      valueListenable: ButtonStatusController
          .status, // listening to any updates in the ButtonStatusController class
      builder: (_, status, __) => GestureDetector(
        // status here is what all the conditional builds in this widget now depend on
        onTap: () {
          if (!status.isBusy && !status.isDisabled) {
            onTap();
          }
        },
        child: block
            ? AnimatedContainer(
                duration: const Duration(milliseconds: 250),
                padding: EdgeInsets.symmetric(vertical: 12.0, horizontal: 20.0),
                width: double.infinity,
                alignment: Alignment.center,
                decoration: !outline
                    ? BoxDecoration(
                        color: !status.isDisabled
                            ? backgroundColor
                            : backgroundColor?.withOpacity(0.5),
                        borderRadius: BorderRadius.circular(!flat ? 8 : 0),
                      )
                    : BoxDecoration(
                        color: Colors.transparent,
                        borderRadius: BorderRadius.circular(!flat ? 8 : 0),
                        border: Border.all(
                          color: !status.isDisabled
                              ? backgroundColor!
                              : backgroundColor!.withOpacity(0.5),
                          width: 1,
                        ),
                      ),
                child: !status.isBusy
                    ? Row(
                        mainAxisSize: MainAxisSize.min,
                        children: [
                          if (leading != null) leading!,
                          if (leading != null) SizedBox(width: 5),
                          Text(
                            label,
                            style: TextStyl.button(context)?.copyWith(
                              fontWeight:
                                  !outline ? FontWeight.bold : FontWeight.w400,
                              color: !outline
                                  ? color != null
                                      ? color
                                      : getContrastColor(backgroundColor!)
                                  : backgroundColor,
                            ),
                          ),
                        ],
                      )
                    : loadingIcon != null
                        ? SizedBox(height: 20, width: 20, child: loadingIcon)
                        : LoadingIcon(
                            color: !outline
                                ? color != null
                                    ? color
                                    : getContrastColor(backgroundColor!)
                                : backgroundColor,
                            height: 16,
                          ),
              )
            : Row(
                crossAxisAlignment: CrossAxisAlignment.center,
                mainAxisSize: MainAxisSize.min,
                children: [
                  AnimatedContainer(
                    duration: const Duration(milliseconds: 250),
                    padding:
                        EdgeInsets.symmetric(vertical: 12.0, horizontal: 20.0),
                    alignment: Alignment.center,
                    decoration: !outline
                        ? BoxDecoration(
                            color: !status.isDisabled
                                ? backgroundColor!
                                : backgroundColor!.withOpacity(0.5),
                            borderRadius: BorderRadius.circular(!flat ? 8 : 0),
                            border: Border.all(
                              color: backgroundColor!,
                              width: 1,
                            ),
                          )
                        : BoxDecoration(
                            color: Colors.transparent,
                            borderRadius: BorderRadius.circular(!flat ? 8 : 0),
                            border: Border.all(
                              color: backgroundColor!,
                              width: 1,
                            ),
                          ),
                    child: !status.isBusy
                        ? Row(
                            mainAxisSize: MainAxisSize.min,
                            children: [
                              if (leading != null) leading!,
                              if (leading != null) SizedBox(width: 5),
                              Text(
                                label,
                                style: TextStyl.button(context)?.copyWith(
                                  fontWeight: !outline
                                      ? FontWeight.bold
                                      : FontWeight.w400,
                                  color: !outline
                                      ? color != null
                                          ? color
                                          : getContrastColor(backgroundColor!)
                                      : backgroundColor!,
                                ),
                              ),
                            ],
                          )
                        : loadingIcon != null
                            ? SizedBox(
                                height: 20, width: 20, child: loadingIcon)
                            : LoadingIcon(
                                color: !outline
                                    ? color != null
                                        ? color
                                        : getContrastColor(backgroundColor!)
                                    : backgroundColor!,
                                height: 16,
                              ),
                  ),
                ],
              ),
      ),
    );
  }
}

Besides that, I suggest reading up on refactoring widgets with large build methods because that one is a bit unruly. It could me made a lot easier to read by breaking it up into smaller StatelessWidgets.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Loren.A