What does that mean: "text_painter.dart: Failed assertion: line 900 pos 12: '!_needsLayout': is not true"?

Viewed 508

I'm using Flutter 2.5.1 and I'm having this error:

  • "text_painter.dart: Failed assertion: line 900 pos 12: '!_needsLayout': is not true"

while using the following code (which generates a switch button, just like IOS, in Flutter). I found it here: pub.dev, and I'm modifying it in order to add null safety. However, I have encountered a bug that I do not understand.

import 'package:flutter/material.dart';
import 'dart:ui';
import 'dart:math';

/// Customable and attractive Switch button.
/// Currently, you can't change the widget
/// width and height properties.
///
/// As well as the classical Switch Widget
/// from flutter material, the following
/// arguments are required:
///
/// * [value] determines whether this switch is on or off.
/// * [onChanged] is called when the user toggles the switch on or off.
class LiteRollingSwitch extends StatefulWidget {
  /// A required boolean that sets the value of the button.
  /// * "on" = `true`
  /// * "off" = `false`
  final bool value;

  /// A function called every time there is a change in state. (required)
  final Function(bool) onChanged;

  /// Text displayed when the [value] is `false`. By default
  /// * "Off"
  final String textOff;

  /// Text displayed when the [value] is `true`. By default:
  /// * "On"
  final String textOn;

  /// Color shown when the [value] is `true`. By default:
  /// * `Colors.green`
  final Color colorOn;

  /// Color shown when the [value] is `false`. By default:
  /// * `Colors.red`
  final Color colorOff;

  /// The size of the text. By default:
  /// * `14.0`
  final double textSize;

  /// The duration of the animation. By default:
  /// * `Duration(milliseconds: 600)`
  final Duration animationDuration;

  /// Text displayed when the [value] is `true`. By default:
  /// * `Icons.check`
  final IconData iconOn;

  /// Text displayed when the [value] is `false`. By default:
  /// * `Icons.flag`
  final IconData iconOff;

  /// The width of the switch. By default:
  /// * `130`
  final double width;

  /// Additional action on tap.
  final Function? onTap;

  /// Additional action on double tap.
  final Function? onDoubleTap;

  /// Additional action on swipe.
  final Function? onSwipe;

  const LiteRollingSwitch({
    Key? key,
    required this.value,
    required this.onChanged,
    this.textOff = "Off",
    this.textOn = "On",
    this.textSize = 14.0,
    this.colorOn = Colors.green,
    this.colorOff = Colors.red,
    this.iconOff = Icons.flag,
    this.iconOn = Icons.check,
    this.animationDuration = const Duration(milliseconds: 600),
    this.width = 130,
    this.onTap,
    this.onDoubleTap,
    this.onSwipe,
  }) : super(key: key);

  @override
  _RollingSwitchState createState() => _RollingSwitchState();
}

class _RollingSwitchState extends State<LiteRollingSwitch>
    with SingleTickerProviderStateMixin {
  late AnimationController animationController;
  late Animation<double> animation;
  late bool turnState;

  double value = 0.0;

  @override
  void dispose() {
    animationController.dispose();
    super.dispose();
  }

  @override
  void initState() {
    super.initState();
    animationController = AnimationController(
      vsync: this,
      lowerBound: 0.0,
      upperBound: 1.0,
      duration: widget.animationDuration,
    );
    animation = CurvedAnimation(
      parent: animationController,
      curve: Curves.easeInOut,
    );
    animationController.addListener(() {
      setState(() {
        value = animation.value;
      });
    });
    turnState = widget.value;
    _determine();
  }

  @override
  Widget build(BuildContext context) {
    Color transitionColor = Color.lerp(widget.colorOff, widget.colorOn, value)!;

    return GestureDetector(
      onDoubleTap: () {
        _action();
        if (widget.onDoubleTap != null) widget.onDoubleTap!();
      },
      onTap: () {
        _action();
        if (widget.onTap != null) widget.onTap!();
      },
      onPanEnd: (details) {
        _action();
        if (widget.onSwipe != null) widget.onSwipe!();
      },
      child: Container(
        padding: const EdgeInsets.all(5),
        width: widget.width,
        decoration: BoxDecoration(
          color: transitionColor,
          borderRadius: BorderRadius.circular(50),
        ),
        child: Stack(
          children: <Widget>[
            // the "off" text
            Transform.translate(
              offset: Offset(10 * value, 0),
              child: Opacity(
                // its opacity will change
                opacity: (1 - value).clamp(0.0, 1.0),
                child: Container(
                  // it's in a container
                  padding: const EdgeInsets.only(right: 5),
                  alignment: Alignment.centerRight,
                  height: 40,
                  child: Text(
                    widget.textOff,
                    style: TextStyle(
                      color: Colors.white,
                      fontWeight: FontWeight.bold,
                      fontSize: widget.textSize,
                    ),
                  ),
                ),
              ),
            ),
            // the "on" text
            Transform.translate(
              offset: Offset(10 * (1 - value), 0),
              child: Opacity(
                opacity: value.clamp(0.0, 1.0),
                child: Container(
                  padding: const EdgeInsets.only(left: 5),
                  alignment: Alignment.centerLeft,
                  height: 40,
                  child: Text(
                    widget.textOn,
                    style: TextStyle(
                      color: Colors.white,
                      fontWeight: FontWeight.bold,
                      fontSize: widget.textSize,
                    ),
                  ),
                ),
              ),
            ),
            // The icons
            Transform.translate(
              offset: Offset(widget.width / 2 * value, 0),
              child: Transform.rotate(
                angle: lerpDouble(0, 2 * pi, value)!,
                // the white thing that will move
                child: Container(
                  height: 40,
                  width: 40,
                  alignment: Alignment.center,
                  decoration: const BoxDecoration(
                    shape: BoxShape.circle,
                    color: Colors.white,
                  ),
                  child: Stack(
                    children: <Widget>[
                      // The "on" icon
                      Center(
                        child: Opacity(
                          opacity: value.clamp(0.0, 1.0),
                          child: Icon(
                            widget.iconOn,
                            size: 21,
                            color: transitionColor,
                          ),
                        ),
                      ),
                      // The "off" icon
                      Center(
                        child: Opacity(
                          opacity: (1 - value).clamp(0.0, 1.0),
                          child: Icon(
                            widget.iconOff,
                            size: 21,
                            color: transitionColor,
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            )
          ],
        ),
      ),
    );
  }

  _action() {
    _determine(changeState: true);
  }

  /// Handles the animation.
  _determine({bool changeState = false}) {
    setState(() {
      if (changeState) turnState = !turnState;
      (turnState)
          ? animationController.forward()
          : animationController.reverse();

      widget.onChanged(turnState);
    });
  }
}

This happens when I click on the "off" icon of the switch:

enter image description here

NOTE The switcher works, but you need to understand that my error doesn't always occur. In fact, it occurs just sometimes, when I try to turn off the switcher (and only when I click very precisely on the icon)

Please help me

1 Answers

Try move the _action function to only be called when onTap, onDoubleTap or onPanEnd isn't null.

onDoubleTap: () {
  if (widget.onDoubleTap != null) {
    widget.onDoubleTap!();
    _action();
  }
},
onTap: () {
  if (widget.onTap != null) {
    _action();
    widget.onTap!();
  }
},
onPanEnd: (details) {
  if (widget.onSwipe != null) {
    widget.onSwipe!();
    _action();
  }
},

Full code example:

import 'package:flutter/material.dart';
import 'dart:ui';
import 'dart:math';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  final String title;

  const MyHomePage({
    Key? key,
    required this.title,
  }) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: LiteRollingSwitch(
          onChanged: (value) {},
          onTap: () {},
          value: false,
        ),
      ),
    );
  }
}

/// Customable and attractive Switch button.
/// Currently, you can't change the widget
/// width and height properties.
///
/// As well as the classical Switch Widget
/// from flutter material, the following
/// arguments are required:
///
/// * [value] determines whether this switch is on or off.
/// * [onChanged] is called when the user toggles the switch on or off.
class LiteRollingSwitch extends StatefulWidget {
  /// A required boolean that sets the value of the button.
  /// * "on" = `true`
  /// * "off" = `false`
  final bool value;

  /// A function called every time there is a change in state. (required)
  final Function(bool) onChanged;

  /// Text displayed when the [value] is `false`. By default
  /// * "Off"
  final String textOff;

  /// Text displayed when the [value] is `true`. By default:
  /// * "On"
  final String textOn;

  /// Color shown when the [value] is `true`. By default:
  /// * `Colors.green`
  final Color colorOn;

  /// Color shown when the [value] is `false`. By default:
  /// * `Colors.red`
  final Color colorOff;

  /// The size of the text. By default:
  /// * `14.0`
  final double textSize;

  /// The duration of the animation. By default:
  /// * `Duration(milliseconds: 600)`
  final Duration animationDuration;

  /// Text displayed when the [value] is `true`. By default:
  /// * `Icons.check`
  final IconData iconOn;

  /// Text displayed when the [value] is `false`. By default:
  /// * `Icons.flag`
  final IconData iconOff;

  /// The width of the switch. By default:
  /// * `130`
  final double width;

  /// Additional action on tap.
  final Function? onTap;

  /// Additional action on double tap.
  final Function? onDoubleTap;

  /// Additional action on swipe.
  final Function? onSwipe;

  const LiteRollingSwitch({
    Key? key,
    required this.value,
    required this.onChanged,
    this.textOff = "Off",
    this.textOn = "On",
    this.textSize = 14.0,
    this.colorOn = Colors.green,
    this.colorOff = Colors.red,
    this.iconOff = Icons.flag,
    this.iconOn = Icons.check,
    this.animationDuration = const Duration(milliseconds: 600),
    this.width = 140,
    this.onTap,
    this.onDoubleTap,
    this.onSwipe,
  }) : super(key: key);

  @override
  _RollingSwitchState createState() => _RollingSwitchState();
}

class _RollingSwitchState extends State<LiteRollingSwitch>
    with SingleTickerProviderStateMixin {
  late AnimationController animationController;
  late Animation<double> animation;
  late bool turnState;

  double value = 0.0;

  @override
  void dispose() {
    animationController.dispose();
    super.dispose();
  }

  @override
  void initState() {
    super.initState();
    animationController = AnimationController(
      vsync: this,
      lowerBound: 0.0,
      upperBound: 1.0,
      duration: widget.animationDuration,
    );
    animation = CurvedAnimation(
      parent: animationController,
      curve: Curves.easeInOut,
    );
    animationController.addListener(() {
      setState(() {
        value = animation.value;
      });
    });
    turnState = widget.value;
    _determine();
  }

  @override
  Widget build(BuildContext context) {
    Color transitionColor = Color.lerp(widget.colorOff, widget.colorOn, value)!;

    return GestureDetector(
      onDoubleTap: () {
        if (widget.onDoubleTap != null) {
          widget.onDoubleTap!();
          _action();
        }
      },
      onTap: () {
        if (widget.onTap != null) {
          _action();
          widget.onTap!();
        }
      },
      onPanEnd: (details) {
        if (widget.onSwipe != null) {
          widget.onSwipe!();
          _action();
        }
      },
      child: Container(
        padding: const EdgeInsets.all(5),
        width: widget.width,
        decoration: BoxDecoration(
          color: transitionColor,
          borderRadius: BorderRadius.circular(50),
        ),
        child: Stack(
          children: <Widget>[
            // the "off" text
            Transform.translate(
              offset: Offset(10 * value, 0),
              child: Opacity(
                // its opacity will change
                opacity: (1 - value).clamp(0.0, 1.0),
                child: Container(
                  // it's in a container
                  padding: const EdgeInsets.only(right: 5),
                  alignment: Alignment.centerRight,
                  height: 40,
                  child: Text(
                    widget.textOff,
                    style: TextStyle(
                      color: Colors.white,
                      fontWeight: FontWeight.bold,
                      fontSize: widget.textSize,
                    ),
                  ),
                ),
              ),
            ),
            // the "on" text
            Transform.translate(
              offset: Offset(10 * (1 - value), 0),
              child: Opacity(
                opacity: value.clamp(0.0, 1.0),
                child: Container(
                  padding: const EdgeInsets.only(left: 5),
                  alignment: Alignment.centerLeft,
                  height: 40,
                  child: Text(
                    widget.textOn,
                    style: TextStyle(
                      color: Colors.white,
                      fontWeight: FontWeight.bold,
                      fontSize: widget.textSize,
                    ),
                  ),
                ),
              ),
            ),
            // The icons
            Transform.translate(
              offset: Offset(widget.width / 1.6 * value, 0),
              child: Transform.rotate(
                angle: lerpDouble(0, 2 * pi, value)!,
                // the white thing that will move
                child: Container(
                  height: 40,
                  width: 40,
                  alignment: Alignment.center,
                  decoration: const BoxDecoration(
                    shape: BoxShape.circle,
                    color: Colors.white,
                  ),
                  child: Stack(
                    children: <Widget>[
                      // The "on" icon
                      Center(
                        child: Opacity(
                          opacity: value.clamp(0.0, 1.0),
                          child: Icon(
                            widget.iconOn,
                            size: 21,
                            color: transitionColor,
                          ),
                        ),
                      ),
                      // The "off" icon
                      Center(
                        child: Opacity(
                          opacity: (1 - value).clamp(0.0, 1.0),
                          child: Icon(
                            widget.iconOff,
                            size: 21,
                            color: transitionColor,
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            )
          ],
        ),
      ),
    );
  }

  _action() {
    _determine(changeState: true);
  }

  /// Handles the animation.
  _determine({bool changeState = false}) {
    debugPrint(changeState.toString());
    setState(() {
      if (changeState) turnState = !turnState;
      (turnState)
          ? animationController.forward()
          : animationController.reverse();

      widget.onChanged(turnState);
    });
  }
}

Related