perf: critical Compose recomposition hotspot fixes#2049
Merged
theovilardo merged 1 commit intoMay 19, 2026
Conversation
Targets the Critical / High items from section 4 (UI/Compose
performance) of the multi-agent codebase review. Each fix below is
small and isolated; the build still produces a passing 302-test
unit suite and a clean :app:assembleDebug.
LibraryScreen.kt
- Folder tab itemsToShow / songsToShow: .toImmutableList() now runs
INSIDE the remember block instead of being chained outside it, and
showPlaylistCards is added to the key set (the previous key list
missed playlistMode, so the cached list could go stale on toggle).
- rememberPagerState was being called in two branches of an if/else,
which loses scroll state when libraryNavigationMode toggles between
COMPACT_PILL and the full tab strip. Replaced by a single call with
a mode-aware initialPage + pageCount lambda.
- Gradient color lists used inline listOf().toImmutableList() on every
recomp. Wrapped in remember(dm, primaryContainer, onPrimaryContainer)
using persistentListOf so the list isn't re-allocated per frame.
- getSelectionIndex bound method reference is now hoisted into a
remember(multiSelectionState) so the same lambda identity is passed
to LibrarySongsTab / LibraryFavoritesTab / LibraryFoldersTab on every
recomposition. Previously each tab received a fresh lambda, breaking
Compose parameter stability on those tabs.
LibraryMediaTabs.kt
- getAlbumColorSchemeFlow(uri) was called inside the items lambda for
every visible album on every recomposition. Each call synchronizes
pendingAlbumColorSchemeLock and dispatcher-launches a generation
coroutine on a cache miss. Wrapped in remember(artUri) so the lookup
is amortized per album.
- Placeholder branches were allocating a fresh
MutableStateFlow<ColorSchemePair?>(null) on every render. Replaced
with a single file-scope EMPTY_ALBUM_COLOR_SCHEME_FLOW.
LibraryPlaybackAwareSongItem.kt + LibrarySongsTab.kt + LibrarySongsAndFavoritesTabs.kt
- Each item used to spin up its own stablePlayerState.map{}.
distinctUntilChanged() collector. With 100+ items visible across
tabs + paging buffers that is 100+ upstream subscriptions each
checking every emission. Lifted the collection into the parent
tabs as a single LibraryPlaybackHints(currentSongId, isPlaying)
flow; items now receive that one hints instance.
CastBottomSheet.kt
- stablePlayerState was being collected just to read isPlaying. Sliced
with .map { it.isPlaying }.distinctUntilChanged() so position ticks
(~4×/s) don't recompose the sheet.
- availableRoutes / bluetoothDevices / activeBluetoothName / devices
derived lists were rebuilt inline on every recomposition. Wrapped
each in remember(inputs). activeDevice was deliberately left inline
because it captures stringResource (composable-only).
QueueBottomSheet.kt
- Hallazgo 3 reappeared in QueuePlaylistSongItem: six independent
animateDpAsState / animateColorAsState / animateFloatAsState calls
per visible queue item. Consolidated into a single updateTransition
keyed on a QueueItemAnimState(isCurrentSong, isDragging,
isSwipeTargeted) — same pattern that was applied to
EnhancedSongListItem. dismissIconAlpha now derives from
revealProgress × an animated factor, so revealProgress can be a
plain float instead of needing its own animation.
- queue param signature: List<Song> -> ImmutableList<Song>. The
caller in UnifiedPlayerOverlaysLayer already had it as
ImmutableList; the downcast there was erasing stability info.
PlayerViewModel.kt + downstream composables
- currentSongArtists: StateFlow<List<Artist>> -> StateFlow<
ImmutableList<Artist>>. FullPlayerSlice.currentSongArtists and
FullPlayerSlicePart1.currentSongArtists migrated to match.
- FullPlayerSongMetadataSection, SongMetadataDisplaySection,
PlayerSongInfo (all FullPlayerContent.kt) and
PlayerArtistPickerBottomSheet now accept ImmutableList<Artist>.
- SongInfoBottomSheetViewModel.resolvedArtists also moved to
ImmutableList<Artist> for the picker call site.
LyricsSheet.kt
- Seven separate context.dataStore.data.map{} subscriptions
(alignment, translation, romanization, animated, blur enabled,
blur strength, keep-screen-on) collapsed into a single mapped
Flow<LyricsSheetPrefs> with distinctUntilChanged. New file-private
LyricsSheetPrefs data class + Preferences.toLyricsSheetPrefs()
helper. The architectural-violation note (these reads still bypass
UserPreferencesRepository) is documented in a comment; the proper
fix needs new repository flows and is a separate task.
- Removed a duplicate DisposableEffect that registered an identical
second lifecycle observer for keep-screen-on (merge artifact).
- Four remember(state) { derivedStateOf { state.field } } wrappers
on plain captured values (isLoadingLyrics, lyrics, isPlaying,
currentSong) replaced with direct destructuring. derivedStateOf
with no State<T> read inside is dead weight.
FullPlayerContent.kt
- Same derivedStateOf misuse on resolvedArtistId. Calculation reads
only the artists parameter and captured artistId, no State<T>.
Replaced with plain remember(artists, artistId).
DailyMixSection.kt
- DailyMixCard's headerSongs / visibleSongs were calling
songs.take(n).toImmutableList() on every recomposition. Wrapped
both in remember(songs).
Verification
- ./gradlew :app:testDebugUnitTest passes 302 tests, 0 failing.
- ./gradlew :app:assembleDebug succeeds.
What is NOT in this PR
- The List<Song> / List<Album> / List<Playlist> parameter migrations
on PlaylistBottomSheet, PlaylistArtCollage, MultiSelectionBottomSheet,
AlbumMultiSelectionOptionSheet, and PlaylistContainer were tried
and reverted. Each has ~10 call sites across LibraryScreen,
AlbumDetailScreen, ArtistDetailScreen, DailyMixScreen,
GenreDetailScreen, etc. that all pass plain playlist.songs:
List<Song>. Flipping the parameter requires either toImmutableList()
boilerplate at every site (anti-pattern) or migrating
Playlist.songs upstream to ImmutableList. The second migration is
the proper fix and belongs to a separate task once the source data
model is touched.
- The architectural violation in LyricsSheet (direct DataStore reads
bypassing UserPreferencesRepository) is consolidated but not yet
routed through the repository.
- The 13 collectAsStateWithLifecycle calls in CastBottomSheet were
deliberately kept separate. Compose smart-skipping already
invalidates only the slice that changed; consolidating them into a
single combine slice was not an unambiguous win.
Collaborator
Author
|
@copilot review |
Contributor
Reviewed at b891e23. No additional code changes are needed from this follow-up. I also checked Actions runs: the only failure on the base branch was an infra runner shutdown in CodeQL (not a code regression), and recent runs are green/in progress. Local Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
b99468d
into
security/xxe-allowlist-ai-keys-fk
1 check passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Targets the Critical / High items from section 4 (UI/Compose performance) of the multi-agent codebase review.
What changed (10 task buckets)
1.
LibraryScreen.ktrecomp hotspotsitemsToShow/songsToShow: `.toImmutableList()` now runs INSIDE the remember block; `showPlaylistCards` added to the key set (the previous key list missed `playlistMode`).2.
LibraryMediaTabs.kt3.
LibraryPlaybackAwareSongItemN×N flow subscriptions4.
CastBottomSheet.kt5.
QueueBottomSheetitem animations (Hallazgo 3 reappeared)6.
currentSongArtistsstability7. `derivedStateOf` misuses
8. `LyricsSheet` DataStore reads
9. `DailyMixSection` allocations
Verification
What's intentionally NOT in this PR
Reviewer notes