From b34e5319bc031aa631e2dedf9a8d7cd14097d362 Mon Sep 17 00:00:00 2001 From: christianarduino Date: Tue, 3 Jun 2025 07:42:22 +0200 Subject: [PATCH 1/2] feat(like_button): optimistic UI update with rollback on failure --- lib/src/like_button.dart | 58 +++++++++++++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/lib/src/like_button.dart b/lib/src/like_button.dart index 058763f..80038fd 100644 --- a/lib/src/like_button.dart +++ b/lib/src/like_button.dart @@ -448,16 +448,62 @@ class LikeButtonState extends State with TickerProviderStateMixin { Text(text, style: const TextStyle(color: Colors.grey)); } - void onTap() { + Future onTap() async { if (_controller!.isAnimating || _likeCountController!.isAnimating) { return; } - if (widget.onTap != null) { - widget.onTap!(_isLiked ?? true).then((bool? isLiked) { - _handleIsLikeChanged(isLiked); + + final bool? previousIsLiked = _isLiked; + final int? previousLikeCount = _likeCount; + + final bool targetIsLiked = !(_isLiked ?? true); + + // Calcola il nuovo likeCount in base al target + int? newLikeCount = _likeCount; + if (_likeCount != null) { + newLikeCount = targetIsLiked ? _likeCount! + 1 : _likeCount! - 1; + } + + _isLiked = targetIsLiked; + _preLikeCount = previousLikeCount; + _likeCount = newLikeCount; + + if (mounted) { + setState(() { + if (_isLiked!) { + _controller!.reset(); + _controller!.forward(); + } + if (widget.likeCountAnimationType != LikeCountAnimationType.none) { + _likeCountController!.reset(); + _likeCountController!.forward(); + } }); - } else { - _handleIsLikeChanged(!(_isLiked ?? true)); + } + + bool? result; + if (widget.onTap != null) { + result = await widget.onTap!(targetIsLiked); + } + + // Se l'operazione fallisce o ritorna null, revert + if (result != null && result != targetIsLiked) { + _isLiked = previousIsLiked; + _likeCount = previousLikeCount; + _preLikeCount = newLikeCount; + + if (mounted) { + setState(() { + if (_isLiked!) { + _controller!.reset(); + _controller!.forward(); + } + if (widget.likeCountAnimationType != LikeCountAnimationType.none) { + _likeCountController!.reset(); + _likeCountController!.forward(); + } + }); + } } } From 7ebc5a648006c4bbdd7e2787021dcba540c7f47d Mon Sep 17 00:00:00 2001 From: christianarduino Date: Thu, 26 Jun 2025 22:13:07 +0200 Subject: [PATCH 2/2] feat(like_button): add onError callback for optimistic update failure handling --- lib/src/like_button.dart | 66 ++++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/lib/src/like_button.dart b/lib/src/like_button.dart index 80038fd..166eb1c 100644 --- a/lib/src/like_button.dart +++ b/lib/src/like_button.dart @@ -37,6 +37,7 @@ class LikeButton extends StatefulWidget { this.padding, this.countDecoration, this.postFrameCallback, + this.onError, }) : bubblesSize = bubblesSize ?? size * 2.0, circleSize = circleSize ?? size * 0.8, super(key: key); @@ -110,6 +111,9 @@ class LikeButton extends StatefulWidget { /// call back of first frame with LikeButtonState final Function(LikeButtonState state)? postFrameCallback; + /// callback chiamata quando il like ottimistico fallisce + final void Function(bool? previousIsLiked, int? previousLikeCount)? onError; + @override State createState() => LikeButtonState(); } @@ -131,16 +135,19 @@ class LikeButtonState extends State with TickerProviderStateMixin { bool? _isLiked = false; int? _likeCount; int? _preLikeCount; + int? _lastOptimisticLikeCount; bool? get isLiked => _isLiked; int? get likeCount => _likeCount; int? get preLikeCount => _preLikeCount; + + bool _isPerformingOptimisticUpdate = false; + @override void initState() { super.initState(); _isLiked = widget.isLiked; - _likeCount = widget.likeCount; _preLikeCount = _likeCount; @@ -162,28 +169,30 @@ class LikeButtonState extends State with TickerProviderStateMixin { @override void didUpdateWidget(LikeButton oldWidget) { - // Check if isLiked state has changed from props - if (widget.isLiked != oldWidget.isLiked && widget.isLiked != _isLiked) { - // Play animation when isLiked becomes true - if (widget.isLiked == true) { - _playLikeAnimation(); - } - - // Update internal state - _updateLikeState(widget.isLiked); + // Aggiorna lo stato locale solo se il valore esterno cambia davvero + if (widget.isLiked != _isLiked) { + _isLiked = widget.isLiked; + setState(() {}); } // Update like count if changed if (widget.likeCount != _likeCount) { + // Se il valore esterno coincide con quello ottimistico, non rilanciare animazione + final bool isOptimisticMatch = _lastOptimisticLikeCount != null && + widget.likeCount == _lastOptimisticLikeCount; _preLikeCount = _likeCount; _likeCount = widget.likeCount; - // Play like count animation if needed if (widget.likeCountAnimationType != LikeCountAnimationType.none && - _preLikeCount != _likeCount) { + _preLikeCount != _likeCount && + !isOptimisticMatch) { _likeCountController?.reset(); _likeCountController?.forward(); } + // Se il valore esterno conferma quello ottimistico, azzera il flag + if (isOptimisticMatch) { + _lastOptimisticLikeCount = null; + } } // Check if animation duration config changed @@ -243,8 +252,8 @@ class LikeButtonState extends State with TickerProviderStateMixin { animation: _controller!, builder: (BuildContext c, Widget? w) { final Widget likeWidget = - widget.likeBuilder?.call(_isLiked ?? true) ?? - defaultWidgetBuilder(_isLiked ?? true, widget.size); + widget.likeBuilder?.call(_isLiked ?? false) ?? + defaultWidgetBuilder(_isLiked ?? false, widget.size); return Stack( clipBehavior: Clip.none, children: [ @@ -449,28 +458,26 @@ class LikeButtonState extends State with TickerProviderStateMixin { } Future onTap() async { - if (_controller!.isAnimating || _likeCountController!.isAnimating) { + if (_controller!.isAnimating || + _likeCountController!.isAnimating || + _isPerformingOptimisticUpdate) { return; } - + _isPerformingOptimisticUpdate = true; final bool? previousIsLiked = _isLiked; final int? previousLikeCount = _likeCount; - final bool targetIsLiked = !(_isLiked ?? true); - - // Calcola il nuovo likeCount in base al target int? newLikeCount = _likeCount; if (_likeCount != null) { newLikeCount = targetIsLiked ? _likeCount! + 1 : _likeCount! - 1; } - _isLiked = targetIsLiked; _preLikeCount = previousLikeCount; _likeCount = newLikeCount; - + _lastOptimisticLikeCount = _likeCount; if (mounted) { setState(() { - if (_isLiked!) { + if (_isLiked! && !(previousIsLiked ?? false)) { _controller!.reset(); _controller!.forward(); } @@ -480,21 +487,23 @@ class LikeButtonState extends State with TickerProviderStateMixin { } }); } - bool? result; if (widget.onTap != null) { result = await widget.onTap!(targetIsLiked); } - - // Se l'operazione fallisce o ritorna null, revert + _isPerformingOptimisticUpdate = false; + // Se l'operazione fallisce o ritorna null, revert e chiama onError if (result != null && result != targetIsLiked) { + final bool? prevIsLiked = previousIsLiked; + final int? prevLikeCount = previousLikeCount; _isLiked = previousIsLiked; _likeCount = previousLikeCount; _preLikeCount = newLikeCount; - + _lastOptimisticLikeCount = null; if (mounted) { setState(() { - if (_isLiked!) { + // Forza l'animazione anche nel rollback se si torna a like + if (_isLiked! && previousIsLiked == false) { _controller!.reset(); _controller!.forward(); } @@ -504,6 +513,9 @@ class LikeButtonState extends State with TickerProviderStateMixin { } }); } + if (widget.onError != null) { + widget.onError!(prevIsLiked, prevLikeCount); + } } }