refactor(android): extract asCacheFirstStateFlow helper (DRY 3d)
Five cache-first ViewModels all ended their flow pipeline with:
.catch { e -> emit(UiState.Error(ErrorCopy.fromThrowable(e))) }
.stateIn(viewModelScope,
SharingStarted.WhileSubscribed(SHARE_STOP_TIMEOUT_MS),
UiState.Loading)
plus a per-file private const for the 5_000L share-stop timeout.
Extract to a single Flow<UiState<T>> -> StateFlow<UiState<T>> extension
in shared/CacheFirstStateFlow.kt; migrate Library, PlaylistsList,
LikedTab, AddToPlaylist, and Home VMs. Each VM drops the catch +
stateIn + ErrorCopy + SharingStarted imports + the local constant.
AddToPlaylistVM keeps its onStart { refresh; emit Loading } block
in front of the helper -- that re-emit is intentional UX for sheet
re-opens past the 5s subscriber timeout.
This commit is contained in:
@@ -47,7 +47,6 @@ import com.composables.icons.lucide.Heart
|
||||
import com.composables.icons.lucide.History
|
||||
import com.composables.icons.lucide.Lucide
|
||||
import com.composables.icons.lucide.Music
|
||||
import com.fabledsword.minstrel.api.ErrorCopy
|
||||
import com.fabledsword.minstrel.home.data.HomeRepository
|
||||
import com.fabledsword.minstrel.library.widgets.AlbumCard
|
||||
import com.fabledsword.minstrel.library.widgets.ArtistCard
|
||||
@@ -66,6 +65,7 @@ import com.fabledsword.minstrel.playlists.widgets.OfflinePoolCard
|
||||
import com.fabledsword.minstrel.playlists.widgets.PlaylistCard
|
||||
import com.fabledsword.minstrel.playlists.widgets.PlaylistPlaceholderCard
|
||||
import com.fabledsword.minstrel.shared.UiState
|
||||
import com.fabledsword.minstrel.shared.asCacheFirstStateFlow
|
||||
import com.fabledsword.minstrel.shared.widgets.EmptyState
|
||||
import com.fabledsword.minstrel.shared.widgets.HorizontalScrollRow
|
||||
import com.fabledsword.minstrel.shared.widgets.MinstrelTopAppBar
|
||||
@@ -81,7 +81,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
@@ -196,11 +195,8 @@ class HomeViewModel @Inject constructor(
|
||||
status.join()
|
||||
}
|
||||
|
||||
val uiState: StateFlow<UiState<HomeSections>> = combineHomeFlows().stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(SHARE_STOP_TIMEOUT_MS),
|
||||
initialValue = UiState.Loading,
|
||||
)
|
||||
val uiState: StateFlow<UiState<HomeSections>> =
|
||||
combineHomeFlows().asCacheFirstStateFlow(viewModelScope)
|
||||
|
||||
/**
|
||||
* Five home-section flows merged into the sections struct without
|
||||
@@ -229,7 +225,7 @@ class HomeViewModel @Inject constructor(
|
||||
observeHomeSections().combine(playlistsRepository.observeAll()) { sections, playlists ->
|
||||
val merged = sections.copy(playlists = playlists)
|
||||
if (merged.isAllEmpty) UiState.Empty else UiState.Success(merged)
|
||||
}.catch { e -> emit(UiState.Error(ErrorCopy.fromThrowable(e))) }
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Screen ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -2,22 +2,17 @@ package com.fabledsword.minstrel.library.ui
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.fabledsword.minstrel.api.ErrorCopy
|
||||
import com.fabledsword.minstrel.cache.sync.SyncController
|
||||
import com.fabledsword.minstrel.library.data.LibraryRepository
|
||||
import com.fabledsword.minstrel.shared.UiState
|
||||
import com.fabledsword.minstrel.shared.asCacheFirstStateFlow
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val SHARE_STOP_TIMEOUT_MS = 5_000L
|
||||
|
||||
@HiltViewModel
|
||||
class LibraryViewModel @Inject constructor(
|
||||
private val repository: LibraryRepository,
|
||||
@@ -36,14 +31,7 @@ class LibraryViewModel @Inject constructor(
|
||||
UiState.Success(LibraryData(artists, albums))
|
||||
}
|
||||
}
|
||||
.catch { e ->
|
||||
emit(UiState.Error(ErrorCopy.fromThrowable(e)))
|
||||
}
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(SHARE_STOP_TIMEOUT_MS),
|
||||
initialValue = UiState.Loading,
|
||||
)
|
||||
.asCacheFirstStateFlow(viewModelScope)
|
||||
|
||||
/**
|
||||
* Pull-to-refresh entry point. Triggers /api/library/sync via the
|
||||
|
||||
@@ -25,7 +25,6 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.NavHostController
|
||||
import com.fabledsword.minstrel.api.ErrorCopy
|
||||
import com.fabledsword.minstrel.likes.data.LikesRepository
|
||||
import com.fabledsword.minstrel.library.widgets.AlbumCard
|
||||
import com.fabledsword.minstrel.library.widgets.ArtistCard
|
||||
@@ -36,6 +35,7 @@ import com.fabledsword.minstrel.nav.AlbumDetail
|
||||
import com.fabledsword.minstrel.nav.ArtistDetail
|
||||
import com.fabledsword.minstrel.player.PlayerController
|
||||
import com.fabledsword.minstrel.shared.UiState
|
||||
import com.fabledsword.minstrel.shared.asCacheFirstStateFlow
|
||||
import com.fabledsword.minstrel.shared.widgets.TrackRow
|
||||
import com.fabledsword.minstrel.shared.widgets.EmptyState
|
||||
import com.fabledsword.minstrel.shared.widgets.HorizontalScrollRow
|
||||
@@ -46,16 +46,11 @@ import com.fabledsword.minstrel.shared.widgets.TrackCoverThumb
|
||||
import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsButton
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val SHARE_STOP_TIMEOUT_MS = 5_000L
|
||||
|
||||
// ─── State ───────────────────────────────────────────────────────────
|
||||
|
||||
data class LikedSections(
|
||||
@@ -100,13 +95,7 @@ class LikedTabViewModel @Inject constructor(
|
||||
) { artists, albums, tracks ->
|
||||
val sections = LikedSections(artists, albums, tracks)
|
||||
if (sections.isAllEmpty) UiState.Empty else UiState.Success(sections)
|
||||
}
|
||||
.catch { e -> emit(UiState.Error(ErrorCopy.fromThrowable(e))) }
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(SHARE_STOP_TIMEOUT_MS),
|
||||
initialValue = UiState.Loading,
|
||||
)
|
||||
}.asCacheFirstStateFlow(viewModelScope)
|
||||
|
||||
/**
|
||||
* Build a queue from the currently-displayed liked tracks and start
|
||||
|
||||
+2
-14
@@ -21,13 +21,13 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.NavHostController
|
||||
import com.fabledsword.minstrel.api.ErrorCopy
|
||||
import com.fabledsword.minstrel.events.EventsStream
|
||||
import com.fabledsword.minstrel.models.PlaylistRef
|
||||
import com.fabledsword.minstrel.nav.PlaylistDetail
|
||||
import com.fabledsword.minstrel.nav.Playlists
|
||||
import com.fabledsword.minstrel.playlists.data.PlaylistsRepository
|
||||
import com.fabledsword.minstrel.shared.UiState
|
||||
import com.fabledsword.minstrel.shared.asCacheFirstStateFlow
|
||||
import com.fabledsword.minstrel.playlists.widgets.PlaylistCard
|
||||
import com.fabledsword.minstrel.shared.widgets.EmptyState
|
||||
import com.fabledsword.minstrel.shared.widgets.LoadingCentered
|
||||
@@ -35,17 +35,12 @@ import com.fabledsword.minstrel.shared.widgets.MinstrelTopAppBar
|
||||
import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val SHARE_STOP_TIMEOUT_MS = 5_000L
|
||||
|
||||
// ─── State ───────────────────────────────────────────────────────────
|
||||
|
||||
// ─── ViewModel ───────────────────────────────────────────────────────
|
||||
@@ -81,14 +76,7 @@ class PlaylistsListViewModel @Inject constructor(
|
||||
UiState.Success(list)
|
||||
}
|
||||
}
|
||||
.catch { e ->
|
||||
emit(UiState.Error(ErrorCopy.fromThrowable(e)))
|
||||
}
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(SHARE_STOP_TIMEOUT_MS),
|
||||
initialValue = UiState.Loading,
|
||||
)
|
||||
.asCacheFirstStateFlow(viewModelScope)
|
||||
}
|
||||
|
||||
// ─── Screen ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.fabledsword.minstrel.shared
|
||||
|
||||
import com.fabledsword.minstrel.api.ErrorCopy
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
private const val SHARE_STOP_TIMEOUT_MS = 5_000L
|
||||
|
||||
/**
|
||||
* Terminal operator for the cache-first VM idiom: a Flow that maps
|
||||
* cache emissions to `UiState.Success` / `UiState.Empty` becomes a
|
||||
* subscriber-active StateFlow that starts in [UiState.Loading] and
|
||||
* routes any upstream throwable through [ErrorCopy] into
|
||||
* [UiState.Error].
|
||||
*
|
||||
* Replaces the catch + stateIn(viewModelScope, WhileSubscribed,
|
||||
* Loading) tail that every cache-first ViewModel writes today.
|
||||
*/
|
||||
fun <T> Flow<UiState<T>>.asCacheFirstStateFlow(
|
||||
scope: CoroutineScope,
|
||||
): StateFlow<UiState<T>> = this
|
||||
.catch { e -> emit(UiState.Error(ErrorCopy.fromThrowable(e))) }
|
||||
.stateIn(
|
||||
scope = scope,
|
||||
started = SharingStarted.WhileSubscribed(SHARE_STOP_TIMEOUT_MS),
|
||||
initialValue = UiState.Loading,
|
||||
)
|
||||
+2
-12
@@ -2,22 +2,17 @@ package com.fabledsword.minstrel.shared.widgets.trackactions
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.fabledsword.minstrel.api.ErrorCopy
|
||||
import com.fabledsword.minstrel.models.PlaylistRef
|
||||
import com.fabledsword.minstrel.playlists.data.PlaylistsRepository
|
||||
import com.fabledsword.minstrel.shared.UiState
|
||||
import com.fabledsword.minstrel.shared.asCacheFirstStateFlow
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val SHARE_STOP_TIMEOUT_MS = 5_000L
|
||||
|
||||
/**
|
||||
* Backs [AddToPlaylistSheet] with the caller's user-owned playlists.
|
||||
* System playlists (For You / Discover / etc.) are filtered out at
|
||||
@@ -42,10 +37,5 @@ class AddToPlaylistViewModel @Inject constructor(
|
||||
}
|
||||
emit(UiState.Loading)
|
||||
}
|
||||
.catch { e -> emit(UiState.Error(ErrorCopy.fromThrowable(e))) }
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(SHARE_STOP_TIMEOUT_MS),
|
||||
initialValue = UiState.Loading,
|
||||
)
|
||||
.asCacheFirstStateFlow(viewModelScope)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user