I had copy pasted code for an animated button that calls setState as it adds a listener to the animation controller.

This article and SO answer:

(Article) Flutter Laggy Animations: How Not To setState

(SO answer) Flutter animation not working

stated that calling setState is not recommended in this case because it will rebuild the entire UI each time the button is pressed. Makes sense!

Now when I do use the AnimatedBuilder stops working. What am I doing wrong?

The Code:

class _AnimatedButtonState extends State<AnimatedButton>
    with SingleTickerProviderStateMixin {
  final style = GoogleFonts.volkhov(
    color: Colors.black,
    fontSize: 15,
  );
  double _scale;
  AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
        vsync: this,
        duration: Duration(milliseconds: 100),
        lowerBound: 0.0,
        upperBound: 0.1);
    // _controller.addListener(() {
    //   setState(() {});
    // });
  }

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

  @override
  Widget build(BuildContext context) {
    _scale = 1 - _controller.value;

    return GestureDetector(
      onTap: _onTap,
      onTapDown: _onTapDown,
      onTapUp: _onTapUp,
      onTapCancel: _onTapCancel,
      child: AnimatedBuilder(           // HERE HERE
          animation: _controller,
          child: _animatedButtonUI,
          builder: (context, child) {
            return Transform.scale(
              scale: _scale,
              child: child,
            );
          },
      )
    );
  }

  _onTapUp(TapUpDetails details) => _controller.reverse();
  _onTapDown(TapDownDetails details) => _controller.forward();
  _onTapCancel() => _controller.reverse();

  Widget get _animatedButtonUI => Container(
    height: 100,
    width: 250,
    decoration: BoxDecoration(
      borderRadius: BorderRadius.circular(100),
      gradient: LinearGradient(
        begin: Alignment.bottomLeft,
        end: Alignment.bottomRight,
        colors: [Colors.green, Colors.green[400], Colors.green[500]],
      ),
    ),
    child: Center(
      child: Text(
        'Save',
        style: TextStyle(
          fontSize: 25,
          fontWeight: FontWeight.bold,
          color: Colors.black,
        ),
      ),
    ),
  );

Side-note: The reason I found this tip, was because I was trying to make my button more responsive to click/touch. Quick clicks and touches on the phone, did not trigger the animation when I used the setState. Instead it took longer than a casual touch by the user. If you can help me with that as well, I'd appreciate it a lot.


Solution 1: Christopher Moore

The issue is that _scale, which you are using as the value for the animation, is not being updated due to a lack of setState. The only place _scale is being updated is in the stateful widget's build method. The only time build is called again after the first time is when setState is called, leading to _scale not being updated like you wanted.

AnimatedBuilder is essentially the same as you would do by calling setState, but it isolates the rebuild to just the builder of itself rather than the enclosing stateful widget as a whole, which is why it's considered better for performance.

To solve this, simply move _scale = 1 - _controller.value; into the builder of AnimatedBuilder.

@override
Widget build(BuildContext context) {
  //_scale = 1 - _controller.value; NOT HERE
  return GestureDetector(
    onTap: _onTap,
    onTapDown: _onTapDown,
    onTapUp: _onTapUp,
    onTapCancel: _onTapCancel,
    child: AnimatedBuilder(
        animation: _controller,
        child: _animatedButtonUI,
        builder: (context, child) {
          _scale = 1 - _controller.value; //HERE
          return Transform.scale(
            scale: _scale,
            child: child,
          );
        },
    )
  );
}