Skip to content

Commit 3dedec7

Browse files
fix: 🐛 Prevent re-entrant calls to _onComplete during rapid barrier taps (#645)
Add _isCompleting boolean guard to prevent concurrent execution of _onComplete() when rapid barrier taps occur during step transition animations. This prevents null-check crashes from calling reverse() on already-disposed AnimationControllers. Fixes crash when user rapidly taps the barrier during showcase step transitions.
1 parent a5dcbfb commit 3dedec7

2 files changed

Lines changed: 40 additions & 24 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
## [Unreleased]
22

3-
- Fixed [#650](https://github.com/SimformSolutionsPvtLtd/showcaseview/issues/650) - Fix null-check crash in ShowcaseService.getController during didUpdateWidget.
4-
- Fixed [#633](https://github.com/SimformSolutionsPvtLtd/showcaseview/issues/633) - Improve showcase flow by consolidating finish logic and handling missing targets
5-
- Fixed [#633](https://github.com/SimformSolutionsPvtLtd/showcaseview/issues/633) - Added `ShowcaseView.isTargetRendered` to detect whether a showcase target is currently rendered, replacing the 4.x.x `key.currentContext` / `key.currentWidget` check.
3+
- Fixed [#650](https://github.com/SimformSolutionsPvtLtd/showcaseview/issues/650) - Fix null-check crash in ShowcaseService.getController during didUpdateWidget. Fixed by @[vatsaltanna-simformsolutions](https://github.com/vatsaltanna-simformsolutions)
4+
- Fixed [#633](https://github.com/SimformSolutionsPvtLtd/showcaseview/issues/633) - Improve showcase flow by consolidating finish logic and handling missing targets. Fixed by @[vasu-nageshri](https://github.com/vasu-nageshri)
5+
- Fixed [#633](https://github.com/SimformSolutionsPvtLtd/showcaseview/issues/633) - Added `ShowcaseView.isTargetRendered` to detect whether a showcase target is currently rendered, replacing the 4.x.x `key.currentContext` / `key.currentWidget` check. Fixed by @[vatsaltanna-simformsolutions](https://github.com/vatsaltanna-simformsolutions)
6+
- Fixed [#645](https://github.com/SimformSolutionsPvtLtd/showcaseview/issues/645) - Prevent re-entrant calls to _onComplete during rapid barrier taps. Fixed by @[apizon](https://github.com/apizon)
67

78
## [5.0.2]
89

lib/src/showcase/showcase_view.dart

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,10 @@ class ShowcaseView {
195195
/// Whether the manager is mounted and active.
196196
bool _mounted = true;
197197

198+
/// Whether a step completion animation is currently in progress.
199+
/// Used to prevent re-entrant calls to [_onComplete] from rapid barrier taps.
200+
bool _isCompleting = false;
201+
198202
/// Map to store keys for which floating action widget should be hidden.
199203
late final Map<GlobalKey, bool> _hideFloatingWidgetKeys;
200204

@@ -439,6 +443,8 @@ class ShowcaseView {
439443
/// - Updates the overlay to reflect current state
440444
void _changeSequence(ShowcaseProgressType type) {
441445
assert(_activeWidgetId != null, 'Please ensure to call startShowcase.');
446+
// Ignore re-entrant taps while a completion is in progress.
447+
if (_isCompleting) return;
442448
final id = switch (type) {
443449
ShowcaseProgressType.forward => _activeWidgetId! + 1,
444450
ShowcaseProgressType.backward => _activeWidgetId! - 1,
@@ -566,30 +572,39 @@ class ShowcaseView {
566572
///
567573
/// Runs reverse animations and triggers completion callbacks.
568574
Future<void> _onComplete() async {
569-
final currentControllers = _getCurrentActiveControllers;
570-
final controllerLength = currentControllers.length;
571-
if (skipIfTargetNotPresent && controllerLength == 0) {
572-
return;
573-
}
574-
575-
await Future.wait([
576-
for (var i = 0; i < controllerLength; i++)
577-
if (!(currentControllers[i].config.disableScaleAnimation ??
578-
disableScaleAnimation) &&
579-
currentControllers[i].reverseAnimationCallback != null)
580-
currentControllers[i].reverseAnimationCallback!.call(),
581-
]);
575+
// Guard against re-entrant calls (e.g. rapid barrier taps during the
576+
// reverse animation await below), which would call reverse() on already-
577+
// disposed AnimationControllers and trigger a null-check crash.
578+
if (_isCompleting) return;
579+
_isCompleting = true;
580+
try {
581+
final currentControllers = _getCurrentActiveControllers;
582+
final controllerLength = currentControllers.length;
583+
if (skipIfTargetNotPresent && controllerLength == 0) {
584+
return;
585+
}
582586

583-
final activeId = _activeWidgetId ?? -1;
584-
if (activeId < (_ids?.length ?? activeId)) {
585-
onComplete?.call(activeId, _ids![activeId]);
586-
// Call all registered onComplete callbacks
587-
for (final callback in _onCompleteCallbacks) {
588-
callback.call(activeId, _ids![activeId]);
587+
await Future.wait([
588+
for (var i = 0; i < controllerLength; i++)
589+
if (!(currentControllers[i].config.disableScaleAnimation ??
590+
disableScaleAnimation) &&
591+
currentControllers[i].reverseAnimationCallback != null)
592+
currentControllers[i].reverseAnimationCallback!.call(),
593+
]);
594+
595+
final activeId = _activeWidgetId ?? -1;
596+
if (activeId < (_ids?.length ?? activeId)) {
597+
onComplete?.call(activeId, _ids![activeId]);
598+
// Call all registered onComplete callbacks
599+
for (final callback in _onCompleteCallbacks) {
600+
callback.call(activeId, _ids![activeId]);
601+
}
589602
}
590-
}
591603

592-
if (autoPlay) _cancelTimer();
604+
if (autoPlay) _cancelTimer();
605+
} finally {
606+
_isCompleting = false;
607+
}
593608
}
594609

595610
/// Finishes the showcase and triggers all finish callbacks.

0 commit comments

Comments
 (0)