skip to Main Content

My actual app is a lot more complex but I have been able to simplify it down to this example which demonstrates the issue.

I have 2 buttons which are supposed to reflect the same data. In this example, I have 2 like (heart) buttons. They can either be in liked or unliked state. However, both’s like/unliked status needs to match. I am doing this via ChangeNotifier, ChangeNotifierProvider and Consumer.

In the actual app, there are basically multiple screens which have buttons whose visual state all needs to match the same data.

All this works fine so far.

Here’s the problem. I need the button to do some animation on tap. This animation only needs to happen in the tapped button and not the other ones. In this simple example, the button does a pop animation. However, the ChangeNotifier‘s notifyListener prevents the animation from happening as I think it’s basically rebuilding the widget and thus preventing the animation. If I comment out the notifyListener, then the animation works but obviously, the data no longer gets updated and thus other button’s don’t reflect the correct liked/unliked status.

I have tried setting keys to the buttons but that didn’t help. I am lost at how I can solve this. Basically, I need to prevent the tapped button from being affected from the Consumer and notifyListener.

Code:

var b1 = GlobalKey();
var b2 = GlobalKey();
final buttonStateProvider = ButtonStateProvider();

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Center(
      child: ChangeNotifierProvider.value(
        value: buttonStateProvider,
        child: Consumer<ButtonStateProvider>(builder: (context, provider, child) {
          return Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              PoppingButton(
                key: b1,
                liked: provider.isLiked,
                onTap: () {
                  provider.toggleLiked();
                },
              ),
              PoppingButton(
                key: b2,
                liked: provider.isLiked,
                onTap: () {
                  provider.toggleLiked();
                },
              ),
            ],
          );
        }),
      ),
    ),
  );
}

//-------------------

class ButtonStateProvider extends ChangeNotifier {
  bool isLiked = false;
  void toggleLiked() {
    isLiked = !isLiked;
    notifyListeners();
  }
}

class PoppingButton extends StatelessWidget {
  const PoppingButton({super.key, required this.liked, required this.onTap});
  final bool liked;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    var keyForHeart = GlobalKey<PopWidgetState>();
    return GestureDetector(
      onTap: (() {
        keyForHeart.currentState?.pop();
        onTap.call();
      }),
      child: PopWidget(
        key: keyForHeart,
        child: Icon(
          liked ? CupertinoIcons.suit_heart_fill : CupertinoIcons.suit_heart,
          size: 100,
          color: liked ? Colors.pink : Colors.grey,
        ),
      ),
    );
  }
}

class PopWidget extends StatefulWidget {
  const PopWidget({super.key, required this.child});
  final Widget child;
  @override
  State<PopWidget> createState() => PopWidgetState();
}

class PopWidgetState extends State<PopWidget> with SingleTickerProviderStateMixin {
  late AnimationController animationController = AnimationController(duration: const Duration(milliseconds: 50), vsync: this);
  late var scaleAnimation = Tween<double>(begin: 1.0, end: 1.4).animate(CurvedAnimation(parent: animationController, curve: Curves.easeOutSine));

  void pop() {
    animationController.forward();
  }

  @override
  void initState() {
    super.initState();
    animationController.addStatusListener(animationListenerHandler);
  }

  void animationListenerHandler(status) {
    // print("Status: ${animationController.status}");
    switch (animationController.status) {
      case AnimationStatus.completed:
        animationController.reverse();
        break;
      case AnimationStatus.dismissed:
        break;
      default:
    }
  }

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

  @override
  Widget build(BuildContext context) {
    // print("scaleAnimation.value: ${scaleAnimation.value}");
    return AnimatedBuilder(
      animation: animationController,
      builder: ((context, child) {
        return Transform.scale(
          scale: scaleAnimation.value,
          child: widget.child,
        );
      }),
    );
  }
}

UI State:

enter image description here

enter image description here

2

Answers


  1. Add a listener to your ButtonStateProvider and play animation on every state change. PopWidgetState init :

    @override
    void initState() {
      super.initState();
      Provider.of<ButtonStateProvider>(context, listen: false).addListener(() {
        pop();
      });
      animationController.addStatusListener(animationListenerHandler);
    }
    
    Login or Signup to reply.
  2. I have modified your code a little, you don’t need GlobalKey

    I have also add some comments to the changes I made

      final buttonStateProvider = ButtonStateProvider();
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Center(
            child: ChangeNotifierProvider.value(
              value: buttonStateProvider,
              child: Consumer<ButtonStateProvider>(
                  builder: (context, provider, child) {
                return Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    for (int i = 1; i < 3; i++)
                      PoppingButton(
                        id: i,
                        liked: provider.isLiked,
                        tappedId: provider.tappedId,
                        onTap: () => provider.toggleLiked(i),
                      ),
                  ],
                );
              }),
            ),
          ),
        );
      }
    
    //-------------------
    class ButtonStateProvider extends ChangeNotifier {
      bool isLiked = false;
    
      // used to indentfiy the widget to play the scale animation
      int tappedId = 0;
    
      void toggleLiked(int id) {
        isLiked = !isLiked;
        tappedId = id;
        notifyListeners();
      }
    }
    
    class PoppingButton extends StatelessWidget {
      const PoppingButton(
          {super.key,
          required this.liked,
          required this.tappedId,
          required this.onTap,
          required this.id});
      final bool liked;
      final VoidCallback onTap;
      final int id;
      final int tappedId;
    
      @override
      Widget build(BuildContext context) {
        return GestureDetector(
          onTap: (() {
            onTap.call();
          }),
          child: PopWidget(
            key: ValueKey(id),
            id: id,
            tappedId: tappedId,
            isLiked: liked,
            child: Icon(
              liked ? CupertinoIcons.suit_heart_fill : CupertinoIcons.suit_heart,
              size: 100,
              color: liked ? Colors.pink : Colors.grey,
            ),
          ),
        );
      }
    }
    
    class PopWidget extends StatefulWidget {
      const PopWidget(
          {super.key,
          required this.child,
          required this.tappedId,
          required this.isLiked,
          required this.id});
      final Widget child;
      final bool isLiked;
      final int id;
      final int tappedId;
      @override
      State<PopWidget> createState() => PopWidgetState();
    }
    
    class PopWidgetState extends State<PopWidget>
        with SingleTickerProviderStateMixin {
      late AnimationController animationController = AnimationController(
          duration: const Duration(milliseconds: 50), vsync: this);
      late var scaleAnimation = Tween<double>(begin: 1.0, end: 1.4).animate(
          CurvedAnimation(parent: animationController, curve: Curves.easeOutSine));
    
      @override
      void initState() {
        super.initState();
        animationController.addStatusListener(animationListenerHandler);
      }
    
      void animationListenerHandler(status) {
        // print("Status: ${animationController.status}");
        switch (animationController.status) {
          case AnimationStatus.completed:
            animationController.reverse();
            break;
          case AnimationStatus.dismissed:
            break;
          default:
        }
      }
    
      @override
      void dispose() {
        animationController.dispose();
        super.dispose();
      }
    
      // update the animation state when [isLiked] is true
      // and check the id of the tapped widget and the widget id
      @override
      void didUpdateWidget(covariant PopWidget oldWidget) {
        if (widget.isLiked && widget.id == widget.tappedId) {
          animationController.forward();
        }
    
        super.didUpdateWidget(oldWidget);
      }
    
      @override
      Widget build(BuildContext context) {
      // print("scaleAnimation.value: ${scaleAnimation.value}");
        return AnimatedBuilder(
          animation: animationController,
          builder: ((context, child) {
            return Transform.scale(
              scale: scaleAnimation.value,
              child: widget.child,
            );
          }),
        );
      }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search