fix(android): ArtistDetail Play button surfaces failures (audit v3 Bug-7)
playArtist() previously swallowed all exceptions with a comment about a 'future refinement' — if the tracks fetch failed or the artist had no playable tracks, the button looked broken with no feedback. Now: ArtistDetailViewModel exposes a transientMessages Flow backed by a buffered Channel. playArtist sends "Couldn't start playback: <reason>" on Throwable and "No tracks to play for this artist." when the result is empty. ArtistDetailScreen collects the Flow into a Scaffold-level SnackbarHost so users see the failure inline instead of tapping a dead button repeatedly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,8 @@ import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -28,7 +30,9 @@ import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -59,6 +63,12 @@ fun ArtistDetailScreen(
|
||||
viewModel: ArtistDetailViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.transientMessages.collect { msg ->
|
||||
snackbarHostState.showSnackbar(msg)
|
||||
}
|
||||
}
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
@@ -71,6 +81,7 @@ fun ArtistDetailScreen(
|
||||
},
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
) { inner ->
|
||||
PullToRefreshScaffold(
|
||||
onRefresh = { viewModel.refresh().join() },
|
||||
|
||||
+14
-5
@@ -13,10 +13,13 @@ import com.fabledsword.minstrel.nav.DetailSeedCache
|
||||
import com.fabledsword.minstrel.player.PlayerController
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
@@ -45,6 +48,10 @@ class ArtistDetailViewModel @Inject constructor(
|
||||
MutableStateFlow<ArtistDetailUiState>(ArtistDetailUiState.Loading())
|
||||
val uiState: StateFlow<ArtistDetailUiState> = internal.asStateFlow()
|
||||
|
||||
/** Transient one-shot messages for the screen-level snackbar (e.g. Play-button failure). */
|
||||
private val transientMessagesChannel = Channel<String>(Channel.BUFFERED)
|
||||
val transientMessages: Flow<String> = transientMessagesChannel.receiveAsFlow()
|
||||
|
||||
val artistLiked: StateFlow<Boolean> =
|
||||
likes.observeIsLiked(LikesRepository.ENTITY_ARTIST, artistId)
|
||||
.stateIn(
|
||||
@@ -92,7 +99,9 @@ class ArtistDetailViewModel @Inject constructor(
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val tracks = repository.fetchArtistTracks(artistId).shuffled()
|
||||
if (tracks.isNotEmpty()) {
|
||||
if (tracks.isEmpty()) {
|
||||
transientMessagesChannel.trySend("No tracks to play for this artist.")
|
||||
} else {
|
||||
player.setQueue(
|
||||
tracks = tracks,
|
||||
initialIndex = 0,
|
||||
@@ -100,11 +109,11 @@ class ArtistDetailViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
} catch (
|
||||
@Suppress("TooGenericExceptionCaught", "SwallowedException") e: Throwable,
|
||||
@Suppress("TooGenericExceptionCaught") e: Throwable,
|
||||
) {
|
||||
// Silent failure — the play button has no inline error
|
||||
// affordance; user can retry by re-tapping. A future
|
||||
// refinement surfaces this via a snackbar.
|
||||
transientMessagesChannel.trySend(
|
||||
"Couldn't start playback: ${e.message ?: "unknown error"}",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user