fix(android): key UiState Crossfades on state::class to stop refresh flicker
android / Build + lint + test (push) Has been cancelled
android / Build + lint + test (push) Has been cancelled
Pull-to-refresh produced a strong full-screen fade on Library (both tabs) and the Album / Artist / Playlist detail screens because their Crossfades were keyed on the entire state value. A refresh emits a fresh UiState.Success with a NEW data instance (same kind, different content) so Crossfade animated old grid → new grid even though both are the same Success branch — the visible result was a flash that read as "broken/heavy." HomeScreen already keys on `state::class` (4b9d-ish prior fix); apply the same pattern to the four screens that still flicker. Inner content reads the outer `state` directly via `val s = state` so the branch still has access to the typed value. Row-level diffs are owned by LazyVerticalGrid / LazyColumn via item keys, so the visual update is smooth and granular instead of a full fade. Only Loading ↔ Success ↔ Error ↔ Empty transitions animate now — the intended use of Crossfade. Same-kind state updates flow through Compose's normal recomposition.
This commit is contained in:
@@ -109,8 +109,11 @@ private fun AlbumDetailStateContent(
|
||||
playerViewModel: com.fabledsword.minstrel.player.ui.PlayerViewModel,
|
||||
navController: NavHostController,
|
||||
) {
|
||||
Crossfade(targetState = state, label = "album-detail") { s ->
|
||||
when (s) {
|
||||
// Crossfade keyed on `state::class` so a Success → fresh Success
|
||||
// refresh (same kind, new detail) doesn't re-fade the whole body;
|
||||
// only Loading ↔ Success ↔ Error transitions animate.
|
||||
Crossfade(targetState = state::class, label = "album-detail") { _ ->
|
||||
when (val s = state) {
|
||||
is AlbumDetailUiState.Loading ->
|
||||
if (s.seed != null) SeededAlbumLoading(s.seed) else SkeletonTrackList()
|
||||
is AlbumDetailUiState.Error -> EmptyState(
|
||||
|
||||
@@ -87,8 +87,11 @@ fun ArtistDetailScreen(
|
||||
onRefresh = { viewModel.refresh().join() },
|
||||
modifier = Modifier.fillMaxSize().padding(inner),
|
||||
) {
|
||||
Crossfade(targetState = state, label = "artist-detail") { s ->
|
||||
when (s) {
|
||||
// Crossfade keyed on `state::class` so a Success → fresh
|
||||
// Success refresh (same kind, new detail) doesn't re-fade
|
||||
// the body; only Loading ↔ Success ↔ Error animate.
|
||||
Crossfade(targetState = state::class, label = "artist-detail") { _ ->
|
||||
when (val s = state) {
|
||||
is ArtistDetailUiState.Loading ->
|
||||
if (s.seed != null) {
|
||||
SeededArtistLoading(s.seed)
|
||||
|
||||
@@ -142,8 +142,13 @@ private fun ArtistsTab(
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
PullToRefreshScaffold(onRefresh = { viewModel.refresh().join() }) {
|
||||
Crossfade(targetState = state, label = "library-artists") { s ->
|
||||
when (s) {
|
||||
// Crossfade is keyed on `state::class` (not the whole state)
|
||||
// so a refresh that emits a fresh UiState.Success — same kind,
|
||||
// new data — doesn't re-fade the entire grid. Only Loading ↔
|
||||
// Success ↔ Error ↔ Empty transitions animate; row-level diff
|
||||
// is owned by LazyVerticalGrid via its item keys.
|
||||
Crossfade(targetState = state::class, label = "library-artists") { _ ->
|
||||
when (val s = state) {
|
||||
UiState.Loading -> SkeletonArtistsGrid()
|
||||
UiState.Empty -> EmptyState(
|
||||
title = "No artists yet",
|
||||
@@ -170,8 +175,8 @@ private fun AlbumsTab(
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
PullToRefreshScaffold(onRefresh = { viewModel.refresh().join() }) {
|
||||
Crossfade(targetState = state, label = "library-albums") { s ->
|
||||
when (s) {
|
||||
Crossfade(targetState = state::class, label = "library-albums") { _ ->
|
||||
when (val s = state) {
|
||||
UiState.Loading -> SkeletonAlbumsGrid()
|
||||
UiState.Empty -> EmptyState(
|
||||
title = "No albums yet",
|
||||
|
||||
+4
-1
@@ -288,7 +288,10 @@ private fun PlaylistDetailContent(
|
||||
playerViewModel: com.fabledsword.minstrel.player.ui.PlayerViewModel,
|
||||
navController: NavHostController,
|
||||
) {
|
||||
Crossfade(targetState = state, label = "playlist-detail") { s -> when (s) {
|
||||
// Crossfade keyed on `state::class` so refreshes that emit a
|
||||
// fresh Success (same kind, new detail) don't re-fade the body;
|
||||
// only Loading ↔ Success ↔ Error transitions animate.
|
||||
Crossfade(targetState = state::class, label = "playlist-detail") { _ -> when (val s = state) {
|
||||
is PlaylistDetailUiState.Loading ->
|
||||
s.seed?.let { SeededPlaylistLoading(it) } ?: SkeletonPlaylistTrackList()
|
||||
is PlaylistDetailUiState.Error -> ErrorBlock(s.message, viewModel::refresh)
|
||||
|
||||
Reference in New Issue
Block a user