fix(ui): prevent auto-animate from overriding user-initiated sheet animations#3740
fix(ui): prevent auto-animate from overriding user-initiated sheet animations#3740kairosci wants to merge 1 commit into
Conversation
The remember(dismissedBound, expandedBound, collapsedBound, coroutineScope) block in rememberBottomSheetState launched animatable.animateTo() every time any key changed, which could cancel a user-initiated expand animation in progress. This race condition made the queue sheet appear unresponsive after certain window inset or bound changes during normal usage. Fixed by tracking active user animations with a counter and only running the remember-block auto-animate when no user animation is in progress.
📝 WalkthroughWalkthroughThis PR centralizes animation launching in ChangesAnimation Launch Centralization
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/src/main/kotlin/com/metrolist/music/ui/component/BottomSheet.kt`:
- Around line 345-354: The race is caused by incrementing
activeAnimations.intValue inside the coroutine body of the launchAnimation
lambda so recompositions can see 0 before the coroutine starts; move the
increment so it happens synchronously before coroutineScope.launch is invoked
(keep the decrement in the finally block inside the launched coroutine), i.e.
update the launchAnimation closure that references activeAnimations.intValue and
coroutineScope.launch so it increments activeAnimations.intValue immediately
prior to calling coroutineScope.launch (while leaving the existing try/finally
decrement inside the launched coroutine), ensuring calls from
state.expandSoft()/collapseSoft()/dismiss() are observed by the remember(...)
logic that checks activeAnimations.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: bcb241f7-1287-4e44-bf77-6f8de35670dd
📒 Files selected for processing (1)
app/src/main/kotlin/com/metrolist/music/ui/component/BottomSheet.kt
| val launchAnimation: (suspend () -> Unit) -> Unit = { block -> | ||
| coroutineScope.launch { | ||
| activeAnimations.intValue++ | ||
| try { | ||
| block() | ||
| } finally { | ||
| activeAnimations.intValue-- | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Race window: increment activeAnimations before launch, not inside the coroutine.
The counter is incremented inside the launched coroutine body, but coroutineScope.launch on AndroidUiDispatcher.Main is dispatched, not immediate. Between the call to state.expandSoft()/collapseSoft()/dismiss() and the moment the coroutine body runs, activeAnimations.intValue is still 0. If dismissedBound/expandedBound/collapsedBound change in that same frame (e.g., a bottom-inset update fired as part of the same gesture), the remember(dismissedBound, expandedBound, collapsedBound, coroutineScope) block at Line 369 re-runs, sees the counter as 0, and calls animateToAnchor() — which then cancels the user's still-pending animateTo on the same Animatable. That is the exact race #3711 is meant to close; this implementation only narrows the window, it doesn't close it.
Move the increment out of the launched block so it's observed synchronously by any recomposition triggered after the user call returns:
🛠️ Proposed fix
val launchAnimation: (suspend () -> Unit) -> Unit = { block ->
+ activeAnimations.intValue++
coroutineScope.launch {
- activeAnimations.intValue++
try {
block()
} finally {
activeAnimations.intValue--
}
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| val launchAnimation: (suspend () -> Unit) -> Unit = { block -> | |
| coroutineScope.launch { | |
| activeAnimations.intValue++ | |
| try { | |
| block() | |
| } finally { | |
| activeAnimations.intValue-- | |
| } | |
| } | |
| } | |
| val launchAnimation: (suspend () -> Unit) -> Unit = { block -> | |
| activeAnimations.intValue++ | |
| coroutineScope.launch { | |
| try { | |
| block() | |
| } finally { | |
| activeAnimations.intValue-- | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/src/main/kotlin/com/metrolist/music/ui/component/BottomSheet.kt` around
lines 345 - 354, The race is caused by incrementing activeAnimations.intValue
inside the coroutine body of the launchAnimation lambda so recompositions can
see 0 before the coroutine starts; move the increment so it happens
synchronously before coroutineScope.launch is invoked (keep the decrement in the
finally block inside the launched coroutine), i.e. update the launchAnimation
closure that references activeAnimations.intValue and coroutineScope.launch so
it increments activeAnimations.intValue immediately prior to calling
coroutineScope.launch (while leaving the existing try/finally decrement inside
the launched coroutine), ensuring calls from
state.expandSoft()/collapseSoft()/dismiss() are observed by the remember(...)
logic that checks activeAnimations.
Problem
The queue sheet occasionally becomes unresponsive — tapping the queue button shows the tap animation but the queue panel does not open. The issue appears randomly during normal usage and requires a full app restart to recover.
Cause
The
remember(dismissedBound, expandedBound, collapsedBound, coroutineScope)block inrememberBottomSheetStatelaunchesanimatable.animateTo(initialValue, NavigationBarAnimationSpec)as a side effect every time any key changes. The queue sheet'sexpandedBoundkey tracksstate.expandedBound(the Player sheet'sanimatable.upperBound!!) anddismissedBounddepends onWindowInsets.systemBars.asPaddingValues().calculateBottomPadding(). When these values fluctuate during normal usage (inset changes, player bound updates), the auto-animate cancels any in-progress user animation fromexpandSoft()mid-flight, creating a race condition that can leave the sheet in a broken state.Solution
activeAnimationscounter (MutableIntState) that tracks whether user-initiated expand/collapse/dismiss animations are in progresslaunchAnimationwrapper that increments/decrements the counter around all user-initiated animation coroutinesrememberblock's auto-animate now checksactiveAnimations == 0before launching — if the user is actively animating, the auto-animate is skipped and the user's animation continues uninterruptedlaunchAnimationtoBottomSheetStateas a constructor parameter (with a safe default for backward compatibility)Testing
./gradlew :app:assembleFossDebugRelated Issues
Summary by CodeRabbit