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:
2
Answers
Add a listener to your
ButtonStateProvider
and play animation on every state change.PopWidgetState
init
:I have modified your code a little, you don’t need
GlobalKey
I have also add some comments to the changes I made