collect only the visible tab and split the position ticker from playback state

This commit is contained in:
austinried
2026-10-04 00:29:58 +00:00
parent 97ca4230da
commit 8429a4529f
11 changed files with 143 additions and 57 deletions
@@ -47,7 +47,6 @@ data class PlaybackState(
val error: String? = null, val error: String? = null,
val isBuffering: Boolean = false, val isBuffering: Boolean = false,
val isPlaying: Boolean = false, val isPlaying: Boolean = false,
val positionMs: Long = 0,
val durationMs: Long = 0, val durationMs: Long = 0,
val hasNext: Boolean = false, val hasNext: Boolean = false,
val hasPrevious: Boolean = false, val hasPrevious: Boolean = false,
@@ -104,6 +103,9 @@ class PlaybackController(
private val _state = MutableStateFlow(PlaybackState()) private val _state = MutableStateFlow(PlaybackState())
val state: StateFlow<PlaybackState> = _state val state: StateFlow<PlaybackState> = _state
private val _positionMs = MutableStateFlow(0L)
val positionMs: StateFlow<Long> = _positionMs
private var player: PlayerHandle? = null private var player: PlayerHandle? = null
private var connecting = false private var connecting = false
private var pendingPlay: PendingPlay? = null private var pendingPlay: PendingPlay? = null
@@ -690,6 +692,7 @@ class PlaybackController(
clear() clear()
} }
_state.value = PlaybackState() _state.value = PlaybackState()
_positionMs.value = 0L
stopPositionTicker() stopPositionTicker()
} }
@@ -920,6 +923,7 @@ class PlaybackController(
player.durationMs.takeIf { it > 0 } player.durationMs.takeIf { it > 0 }
?: player.currentItem?.durationMs?.takeIf { it > 0 } ?: player.currentItem?.durationMs?.takeIf { it > 0 }
?: 0L ?: 0L
_positionMs.value = player.currentPositionMs
_state.value = _state.value =
PlaybackState( PlaybackState(
item = player.currentItem, item = player.currentItem,
@@ -928,7 +932,6 @@ class PlaybackController(
error = lastError, error = lastError,
isBuffering = showBuffering, isBuffering = showBuffering,
isPlaying = player.playWhenReady && !player.isIdle && !player.isEnded, isPlaying = player.playWhenReady && !player.isIdle && !player.isEnded,
positionMs = player.currentPositionMs,
durationMs = durationMs, durationMs = durationMs,
hasNext = true, hasNext = true,
hasPrevious = true, hasPrevious = true,
@@ -946,7 +949,7 @@ class PlaybackController(
delay(POSITION_TICK_MS) delay(POSITION_TICK_MS)
val player = player ?: break val player = player ?: break
val positionMs = player.currentPositionMs val positionMs = player.currentPositionMs
_state.value = _state.value.copy(positionMs = positionMs) _positionMs.value = positionMs
if (positionMs - lastSavedPositionMs >= POSITION_SAVE_INTERVAL_MS || positionMs < lastSavedPositionMs) { if (positionMs - lastSavedPositionMs >= POSITION_SAVE_INTERVAL_MS || positionMs < lastSavedPositionMs) {
lastSavedPositionMs = positionMs lastSavedPositionMs = positionMs
queueRepository.setPosition(positionMs) queueRepository.setPosition(positionMs)
@@ -290,9 +290,11 @@ private fun MainNavigation() {
enter = expandVertically(tween(MINI_PLAYER_ANIM_MS), expandFrom = Alignment.Bottom), enter = expandVertically(tween(MINI_PLAYER_ANIM_MS), expandFrom = Alignment.Bottom),
exit = shrinkVertically(tween(MINI_PLAYER_ANIM_MS), shrinkTowards = Alignment.Bottom), exit = shrinkVertically(tween(MINI_PLAYER_ANIM_MS), shrinkTowards = Alignment.Bottom),
) { ) {
val positionMs by playbackController.positionMs.collectAsStateWithLifecycle()
val miniArt = playbackController.coverArt(playback.item, thumbnail = true) val miniArt = playbackController.coverArt(playback.item, thumbnail = true)
MiniPlayer( MiniPlayer(
state = playback, state = playback,
positionMs = positionMs,
coverArt = miniArt, coverArt = miniArt,
artwork = artwork, artwork = artwork,
onExpand = { settleNowPlaying(true) }, onExpand = { settleNowPlaying(true) },
@@ -99,7 +99,7 @@ import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.paging.compose.LazyPagingItems import androidx.paging.PagingData
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
import com.subtracks.data.model.Album import com.subtracks.data.model.Album
import com.subtracks.data.model.Artist import com.subtracks.data.model.Artist
@@ -119,6 +119,7 @@ import com.subtracks.ui.theme.ArtworkColors
import com.subtracks.ui.theme.librarySurfaceColor import com.subtracks.ui.theme.librarySurfaceColor
import com.subtracks.ui.theme.playerSurfaceColor import com.subtracks.ui.theme.playerSurfaceColor
import com.subtracks.ui.theme.rememberArtworkColors import com.subtracks.ui.theme.rememberArtworkColors
import kotlinx.coroutines.flow.Flow
import org.koin.compose.koinInject import org.koin.compose.koinInject
import org.koin.compose.viewmodel.koinViewModel import org.koin.compose.viewmodel.koinViewModel
import kotlin.math.abs import kotlin.math.abs
@@ -219,10 +220,10 @@ fun LibraryRoute(
syncing = syncing, syncing = syncing,
bottomInset = bottomInset, bottomInset = bottomInset,
artwork = artwork, artwork = artwork,
albums = viewModel.albums.collectAsLazyPagingItems(), albums = viewModel.albums,
artists = viewModel.artists.collectAsLazyPagingItems(), artists = viewModel.artists,
songs = viewModel.songs.collectAsLazyPagingItems(), songs = viewModel.songs,
playlists = viewModel.playlists.collectAsLazyPagingItems(), playlists = viewModel.playlists,
coverArt = viewModel::coverArt, coverArt = viewModel::coverArt,
onAlbumClick = onAlbumClick, onAlbumClick = onAlbumClick,
onArtistClick = onArtistClick, onArtistClick = onArtistClick,
@@ -260,10 +261,10 @@ fun LibraryRoute(
fun LibraryScreen( fun LibraryScreen(
selectedTab: LibraryTab, selectedTab: LibraryTab,
onTabSelected: (LibraryTab) -> Unit, onTabSelected: (LibraryTab) -> Unit,
albums: LazyPagingItems<Album>, albums: Flow<PagingData<Album>>,
artists: LazyPagingItems<Artist>, artists: Flow<PagingData<Artist>>,
songs: LazyPagingItems<SongListItem>, songs: Flow<PagingData<SongListItem>>,
playlists: LazyPagingItems<Playlist>, playlists: Flow<PagingData<Playlist>>,
coverArt: (String?, Boolean) -> CoverArtRef?, coverArt: (String?, Boolean) -> CoverArtRef?,
onAlbumClick: (Album) -> Unit, onAlbumClick: (Album) -> Unit,
onArtistClick: (Artist) -> Unit, onArtistClick: (Artist) -> Unit,
@@ -356,7 +357,7 @@ fun LibraryScreen(
when (pageTab) { when (pageTab) {
LibraryTab.Albums -> { LibraryTab.Albums -> {
AlbumsContent( AlbumsContent(
albums, albums.collectAsLazyPagingItems(),
coverArt, coverArt,
listBottomInset, listBottomInset,
{ {
@@ -373,7 +374,7 @@ fun LibraryScreen(
LibraryTab.Artists -> { LibraryTab.Artists -> {
ArtistsContent( ArtistsContent(
artists, artists.collectAsLazyPagingItems(),
coverArt, coverArt,
listBottomInset, listBottomInset,
{ {
@@ -390,7 +391,7 @@ fun LibraryScreen(
LibraryTab.Songs -> { LibraryTab.Songs -> {
SongsContent( SongsContent(
songs, songs.collectAsLazyPagingItems(),
coverArt, coverArt,
listBottomInset, listBottomInset,
{ {
@@ -408,7 +409,7 @@ fun LibraryScreen(
LibraryTab.Playlists -> { LibraryTab.Playlists -> {
PlaylistsContent( PlaylistsContent(
playlists, playlists.collectAsLazyPagingItems(),
coverArt, coverArt,
listBottomInset, listBottomInset,
{ {
@@ -52,6 +52,7 @@ private const val PROGRESS_INSET_MS = 260
@Composable @Composable
fun MiniPlayer( fun MiniPlayer(
state: PlaybackState, state: PlaybackState,
positionMs: Long,
coverArt: CoverArtRef?, coverArt: CoverArtRef?,
artwork: ArtworkColors?, artwork: ArtworkColors?,
onExpand: () -> Unit, onExpand: () -> Unit,
@@ -64,7 +65,7 @@ fun MiniPlayer(
) { ) {
val item = state.item ?: return val item = state.item ?: return
var dragUpPx by remember { mutableFloatStateOf(0f) } var dragUpPx by remember { mutableFloatStateOf(0f) }
val progress = if (state.durationMs > 0) (state.positionMs.toFloat() / state.durationMs).coerceIn(0f, 1f) else 0f val progress = if (state.durationMs > 0) (positionMs.toFloat() / state.durationMs).coerceIn(0f, 1f) else 0f
val progressInsetDp by animateDpAsState( val progressInsetDp by animateDpAsState(
targetValue = if (progressInset) PROGRESS_INSET else 0.dp, targetValue = if (progressInset) PROGRESS_INSET else 0.dp,
animationSpec = tween(PROGRESS_INSET_MS), animationSpec = tween(PROGRESS_INSET_MS),
@@ -80,6 +80,7 @@ fun NowPlayingRoute(
controller: PlaybackController = koinInject(), controller: PlaybackController = koinInject(),
) { ) {
val state by controller.state.collectAsStateWithLifecycle() val state by controller.state.collectAsStateWithLifecycle()
val positionMs by controller.positionMs.collectAsStateWithLifecycle()
val context = LocalPlatformContext.current val context = LocalPlatformContext.current
val art = controller.coverArt(state.item) val art = controller.coverArt(state.item)
val thumbnail = controller.coverArt(state.item, thumbnail = true) val thumbnail = controller.coverArt(state.item, thumbnail = true)
@@ -102,6 +103,7 @@ fun NowPlayingRoute(
} }
NowPlayingScreen( NowPlayingScreen(
state = state, state = state,
positionMs = positionMs,
title = sourceTitle.value, title = sourceTitle.value,
coverArt = art, coverArt = art,
thumbnailRef = thumbnail, thumbnailRef = thumbnail,
@@ -136,6 +138,7 @@ private fun prefetchImage(
@Composable @Composable
fun NowPlayingScreen( fun NowPlayingScreen(
state: PlaybackState, state: PlaybackState,
positionMs: Long,
title: String, title: String,
coverArt: CoverArtRef?, coverArt: CoverArtRef?,
artwork: ArtworkColors?, artwork: ArtworkColors?,
@@ -263,7 +266,7 @@ fun NowPlayingScreen(
var dragging by remember { mutableStateOf(false) } var dragging by remember { mutableStateOf(false) }
var dragPosition by remember { mutableFloatStateOf(0f) } var dragPosition by remember { mutableFloatStateOf(0f) }
Slider( Slider(
value = if (dragging) dragPosition else state.positionMs.toFloat(), value = if (dragging) dragPosition else positionMs.toFloat(),
onValueChange = { onValueChange = {
dragging = true dragging = true
dragPosition = it dragPosition = it
@@ -280,7 +283,7 @@ fun NowPlayingScreen(
modifier = Modifier.fillMaxWidth().padding(top = 10.dp), modifier = Modifier.fillMaxWidth().padding(top = 10.dp),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
Text(formatTime(state.positionMs), style = MaterialTheme.typography.bodySmall) Text(formatTime(positionMs), style = MaterialTheme.typography.bodySmall)
Text(formatTime(state.durationMs), style = MaterialTheme.typography.bodySmall) Text(formatTime(state.durationMs), style = MaterialTheme.typography.bodySmall)
} }
if (state.error != null) { if (state.error != null) {
@@ -13,7 +13,10 @@ import com.subtracks.data.model.Source
import com.subtracks.data.prefs.fakeUserPreferences import com.subtracks.data.prefs.fakeUserPreferences
import com.subtracks.data.repo.QueueRepository import com.subtracks.data.repo.QueueRepository
import com.subtracks.data.repo.SourceRepository import com.subtracks.data.repo.SourceRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.junit.After import org.junit.After
@@ -23,6 +26,7 @@ import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.Executors import java.util.concurrent.Executors
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@@ -924,7 +928,7 @@ class PlaybackControllerTest {
?.id == "s1" ?.id == "s1"
} }
assertEquals(12_000L, restored.state.value.positionMs) assertEquals(12_000L, restored.positionMs.value)
assertEquals(100_000L, restored.state.value.durationMs) assertEquals(100_000L, restored.state.value.durationMs)
assertEquals( assertEquals(
"setWindow(size=3, start=0, position=12000)", "setWindow(size=3, start=0, position=12000)",
@@ -953,6 +957,26 @@ class PlaybackControllerTest {
assertEquals(0L, runBlocking { queues.cursorPositionMs() }) assertEquals(0L, runBlocking { queues.cursorPositionMs() })
} }
@Test
fun positionTickerAdvancesPositionWithoutEmittingPlaybackState() =
runBlocking {
seedAlbum(3, sourceId = 1)
controller.playAlbum(1, "al1", 0)
await { controller.state.value.isPlaying }
val emissions = CopyOnWriteArrayList<PlaybackState>()
val collector = launch(Dispatchers.Default) { controller.state.collect { emissions += it } }
await { emissions.isNotEmpty() }
val baseline = emissions.size
handle.positionMs = 7_000L
delay(1_200L)
assertEquals(7_000L, controller.positionMs.value)
assertEquals(baseline, emissions.size)
collector.cancel()
}
private fun seedAlbum( private fun seedAlbum(
count: Int, count: Int,
sourceId: Long, sourceId: Long,
@@ -105,10 +105,10 @@ class AppScreenshotTest {
LibraryScreen( LibraryScreen(
selectedTab = LibraryTab.Albums, selectedTab = LibraryTab.Albums,
onTabSelected = {}, onTabSelected = {},
albums = remember { flowOf(PagingData.from(albums)) }.collectAsLazyPagingItems(), albums = remember { flowOf(PagingData.from(albums)) },
artists = remember { flowOf(PagingData.empty<Artist>()) }.collectAsLazyPagingItems(), artists = remember { flowOf(PagingData.empty<Artist>()) },
songs = remember { flowOf(PagingData.empty<SongListItem>()) }.collectAsLazyPagingItems(), songs = remember { flowOf(PagingData.empty<SongListItem>()) },
playlists = remember { flowOf(PagingData.empty<Playlist>()) }.collectAsLazyPagingItems(), playlists = remember { flowOf(PagingData.empty<Playlist>()) },
coverArt = { id, _ -> id?.let { CoverArtRef(it, "test:$it") } }, coverArt = { id, _ -> id?.let { CoverArtRef(it, "test:$it") } },
onAlbumClick = {}, onAlbumClick = {},
onArtistClick = {}, onArtistClick = {},
@@ -164,10 +164,10 @@ class AppScreenshotTest {
LibraryScreen( LibraryScreen(
selectedTab = LibraryTab.Albums, selectedTab = LibraryTab.Albums,
onTabSelected = {}, onTabSelected = {},
albums = remember { flowOf(PagingData.from(Fixtures.albums)) }.collectAsLazyPagingItems(), albums = remember { flowOf(PagingData.from(Fixtures.albums)) },
artists = remember { flowOf(PagingData.from(Fixtures.artists)) }.collectAsLazyPagingItems(), artists = remember { flowOf(PagingData.from(Fixtures.artists)) },
songs = remember { flowOf(PagingData.from(Fixtures.songItems)) }.collectAsLazyPagingItems(), songs = remember { flowOf(PagingData.from(Fixtures.songItems)) },
playlists = remember { flowOf(PagingData.from(Fixtures.playlists)) }.collectAsLazyPagingItems(), playlists = remember { flowOf(PagingData.from(Fixtures.playlists)) },
coverArt = { id, _ -> id?.let { CoverArtRef(it, "test:$it") } }, coverArt = { id, _ -> id?.let { CoverArtRef(it, "test:$it") } },
onAlbumClick = {}, onAlbumClick = {},
onArtistClick = {}, onArtistClick = {},
@@ -180,6 +180,7 @@ class AppScreenshotTest {
) )
MiniPlayer( MiniPlayer(
state = Fixtures.playbackState(), state = Fixtures.playbackState(),
positionMs = 62_000,
coverArt = CoverArtRef("art-al-kid-a", "test:art-al-kid-a"), coverArt = CoverArtRef("art-al-kid-a", "test:art-al-kid-a"),
artwork = artwork, artwork = artwork,
onExpand = {}, onExpand = {},
@@ -374,6 +375,7 @@ class AppScreenshotTest {
SubtracksTheme { SubtracksTheme {
NowPlayingScreen( NowPlayingScreen(
state = Fixtures.playbackState(), state = Fixtures.playbackState(),
positionMs = 62_000,
title = "Kid A", title = "Kid A",
coverArt = CoverArtRef("art-al-kid-a", "test:art-al-kid-a"), coverArt = CoverArtRef("art-al-kid-a", "test:art-al-kid-a"),
artwork = artworkColorsFromSeed(Color.rgb(120, 80, 200)), artwork = artworkColorsFromSeed(Color.rgb(120, 80, 200)),
@@ -397,6 +399,7 @@ class AppScreenshotTest {
SubtracksTheme { SubtracksTheme {
NowPlayingScreen( NowPlayingScreen(
state = Fixtures.playbackState().copy(shuffle = true, repeat = RepeatMode.One), state = Fixtures.playbackState().copy(shuffle = true, repeat = RepeatMode.One),
positionMs = 62_000,
title = "Kid A", title = "Kid A",
coverArt = CoverArtRef("art-al-kid-a", "test:art-al-kid-a"), coverArt = CoverArtRef("art-al-kid-a", "test:art-al-kid-a"),
artwork = artworkColorsFromSeed(Color.rgb(120, 80, 200)), artwork = artworkColorsFromSeed(Color.rgb(120, 80, 200)),
@@ -448,6 +451,7 @@ class AppScreenshotTest {
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
MiniPlayer( MiniPlayer(
state = Fixtures.playbackState(), state = Fixtures.playbackState(),
positionMs = 62_000,
coverArt = CoverArtRef("art-al-kid-a", "test:art-al-kid-a"), coverArt = CoverArtRef("art-al-kid-a", "test:art-al-kid-a"),
artwork = artworkColorsFromSeed(Color.rgb(120, 80, 200)), artwork = artworkColorsFromSeed(Color.rgb(120, 80, 200)),
onExpand = {}, onExpand = {},
@@ -467,10 +471,10 @@ class AppScreenshotTest {
LibraryScreen( LibraryScreen(
selectedTab = tab, selectedTab = tab,
onTabSelected = {}, onTabSelected = {},
albums = remember { flowOf(PagingData.from(Fixtures.albums)) }.collectAsLazyPagingItems(), albums = remember { flowOf(PagingData.from(Fixtures.albums)) },
artists = remember { flowOf(PagingData.from(Fixtures.artists)) }.collectAsLazyPagingItems(), artists = remember { flowOf(PagingData.from(Fixtures.artists)) },
songs = remember { flowOf(PagingData.from(Fixtures.songItems)) }.collectAsLazyPagingItems(), songs = remember { flowOf(PagingData.from(Fixtures.songItems)) },
playlists = remember { flowOf(PagingData.from(Fixtures.playlists)) }.collectAsLazyPagingItems(), playlists = remember { flowOf(PagingData.from(Fixtures.playlists)) },
coverArt = { id, _ -> id?.let { CoverArtRef(it, "test:$it") } }, coverArt = { id, _ -> id?.let { CoverArtRef(it, "test:$it") } },
onAlbumClick = {}, onAlbumClick = {},
onArtistClick = {}, onArtistClick = {},
@@ -562,7 +566,6 @@ private object Fixtures {
coverArtId = "art-al-kid-a", coverArtId = "art-al-kid-a",
), ),
isPlaying = true, isPlaying = true,
positionMs = 62_000,
durationMs = 251_000, durationMs = 251_000,
hasNext = true, hasNext = true,
hasPrevious = false, hasPrevious = false,
@@ -8,7 +8,6 @@ import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.paging.PagingData import androidx.paging.PagingData
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers
import com.github.takahirom.roborazzi.captureRoboImage import com.github.takahirom.roborazzi.captureRoboImage
@@ -43,10 +42,10 @@ class LibraryOptionsScreenshotTest {
LibraryScreen( LibraryScreen(
selectedTab = LibraryTab.Albums, selectedTab = LibraryTab.Albums,
onTabSelected = {}, onTabSelected = {},
albums = remember { flowOf(PagingData.from(albums())) }.collectAsLazyPagingItems(), albums = remember { flowOf(PagingData.from(albums())) },
artists = remember { flowOf(PagingData.empty<Artist>()) }.collectAsLazyPagingItems(), artists = remember { flowOf(PagingData.empty<Artist>()) },
songs = remember { flowOf(PagingData.empty<SongListItem>()) }.collectAsLazyPagingItems(), songs = remember { flowOf(PagingData.empty<SongListItem>()) },
playlists = remember { flowOf(PagingData.empty<Playlist>()) }.collectAsLazyPagingItems(), playlists = remember { flowOf(PagingData.empty<Playlist>()) },
coverArt = { _, _ -> null }, coverArt = { _, _ -> null },
onAlbumClick = {}, onAlbumClick = {},
onArtistClick = {}, onArtistClick = {},
@@ -12,7 +12,6 @@ import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.paging.PagingData import androidx.paging.PagingData
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers
import com.subtracks.data.model.Album import com.subtracks.data.model.Album
@@ -49,10 +48,10 @@ class LibraryOptionsTest {
LibraryScreen( LibraryScreen(
selectedTab = LibraryTab.Albums, selectedTab = LibraryTab.Albums,
onTabSelected = {}, onTabSelected = {},
albums = remember { flowOf(PagingData.from(albums())) }.collectAsLazyPagingItems(), albums = remember { flowOf(PagingData.from(albums())) },
artists = remember { flowOf(PagingData.empty<Artist>()) }.collectAsLazyPagingItems(), artists = remember { flowOf(PagingData.empty<Artist>()) },
songs = remember { flowOf(PagingData.empty<SongListItem>()) }.collectAsLazyPagingItems(), songs = remember { flowOf(PagingData.empty<SongListItem>()) },
playlists = remember { flowOf(PagingData.empty<Playlist>()) }.collectAsLazyPagingItems(), playlists = remember { flowOf(PagingData.empty<Playlist>()) },
coverArt = { _, _ -> null }, coverArt = { _, _ -> null },
onAlbumClick = {}, onAlbumClick = {},
onArtistClick = {}, onArtistClick = {},
@@ -15,7 +15,6 @@ import androidx.compose.ui.test.swipeDown
import androidx.paging.LoadState import androidx.paging.LoadState
import androidx.paging.LoadStates import androidx.paging.LoadStates
import androidx.paging.PagingData import androidx.paging.PagingData
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers
import com.subtracks.data.model.Album import com.subtracks.data.model.Album
@@ -27,13 +26,17 @@ import com.subtracks.ui.library.LibraryScreen
import com.subtracks.ui.library.LibraryTab import com.subtracks.ui.library.LibraryTab
import com.subtracks.ui.library.LibraryTabs import com.subtracks.ui.library.LibraryTabs
import com.subtracks.ui.theme.SubtracksTheme import com.subtracks.ui.theme.SubtracksTheme
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
import org.robolectric.annotation.GraphicsMode import org.robolectric.annotation.GraphicsMode
import java.util.concurrent.CopyOnWriteArrayList
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE) @GraphicsMode(GraphicsMode.Mode.NATIVE)
@@ -51,10 +54,10 @@ class LibraryScreenTest {
LibraryScreen( LibraryScreen(
selectedTab = LibraryTab.Albums, selectedTab = LibraryTab.Albums,
onTabSelected = {}, onTabSelected = {},
albums = remember { flowOf(PagingData.from(albums())) }.collectAsLazyPagingItems(), albums = remember { flowOf(PagingData.from(albums())) },
artists = remember { flowOf(PagingData.empty<Artist>()) }.collectAsLazyPagingItems(), artists = remember { flowOf(PagingData.empty<Artist>()) },
songs = remember { flowOf(PagingData.empty<SongListItem>()) }.collectAsLazyPagingItems(), songs = remember { flowOf(PagingData.empty<SongListItem>()) },
playlists = remember { flowOf(PagingData.empty<Playlist>()) }.collectAsLazyPagingItems(), playlists = remember { flowOf(PagingData.empty<Playlist>()) },
coverArt = { _, _ -> null }, coverArt = { _, _ -> null },
onAlbumClick = {}, onAlbumClick = {},
onArtistClick = {}, onArtistClick = {},
@@ -86,10 +89,10 @@ class LibraryScreenTest {
LibraryScreen( LibraryScreen(
selectedTab = LibraryTab.Albums, selectedTab = LibraryTab.Albums,
onTabSelected = {}, onTabSelected = {},
albums = remember { flowOf(PagingData.empty<Album>(settledLoadStates)) }.collectAsLazyPagingItems(), albums = remember { flowOf(PagingData.empty<Album>(settledLoadStates)) },
artists = remember { flowOf(PagingData.empty<Artist>()) }.collectAsLazyPagingItems(), artists = remember { flowOf(PagingData.empty<Artist>()) },
songs = remember { flowOf(PagingData.empty<SongListItem>()) }.collectAsLazyPagingItems(), songs = remember { flowOf(PagingData.empty<SongListItem>()) },
playlists = remember { flowOf(PagingData.empty<Playlist>()) }.collectAsLazyPagingItems(), playlists = remember { flowOf(PagingData.empty<Playlist>()) },
coverArt = { _, _ -> null }, coverArt = { _, _ -> null },
onAlbumClick = {}, onAlbumClick = {},
onArtistClick = {}, onArtistClick = {},
@@ -109,6 +112,55 @@ class LibraryScreenTest {
assertTrue("an empty library should offer a sync", synced) assertTrue("an empty library should offer a sync", synced)
} }
@Test
fun onlyTheVisibleTabCollectsItsPagingFlow() {
val collected = CopyOnWriteArrayList<String>()
val albums =
flow<PagingData<Album>> {
collected += "albums"
emit(PagingData.from(albums()))
}
val artists =
flow<PagingData<Artist>> {
collected += "artists"
emit(PagingData.empty())
}
val songs =
flow<PagingData<SongListItem>> {
collected += "songs"
emit(PagingData.empty())
}
val playlists =
flow<PagingData<Playlist>> {
collected += "playlists"
emit(PagingData.empty())
}
composeRule.setContent {
SubtracksTheme {
LibraryScreen(
selectedTab = LibraryTab.Albums,
onTabSelected = {},
albums = albums,
artists = artists,
songs = songs,
playlists = playlists,
coverArt = { _, _ -> null },
onAlbumClick = {},
onArtistClick = {},
onPlaylistClick = {},
onSongClick = {},
onSync = {},
onOpenSettings = {},
)
}
}
composeRule.waitUntil(timeoutMillis = 5_000) { collected.isNotEmpty() }
composeRule.waitForIdle()
assertEquals(listOf("albums"), collected.toList())
}
@Test @Test
fun theTabRowReflowsInOneDirectionDuringASwitch() { fun theTabRowReflowsInOneDirectionDuringASwitch() {
val pagerState = PagerState(currentPage = 1, pageCount = { LibraryTab.entries.size }) val pagerState = PagerState(currentPage = 1, pageCount = { LibraryTab.entries.size })
@@ -9,7 +9,6 @@ import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.paging.PagingData import androidx.paging.PagingData
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers
import com.github.takahirom.roborazzi.captureRoboImage import com.github.takahirom.roborazzi.captureRoboImage
@@ -44,10 +43,10 @@ class LibrarySearchScreenshotTest {
LibraryScreen( LibraryScreen(
selectedTab = LibraryTab.Albums, selectedTab = LibraryTab.Albums,
onTabSelected = {}, onTabSelected = {},
albums = remember { flowOf(PagingData.from(albums())) }.collectAsLazyPagingItems(), albums = remember { flowOf(PagingData.from(albums())) },
artists = remember { flowOf(PagingData.empty<Artist>()) }.collectAsLazyPagingItems(), artists = remember { flowOf(PagingData.empty<Artist>()) },
songs = remember { flowOf(PagingData.empty<SongListItem>()) }.collectAsLazyPagingItems(), songs = remember { flowOf(PagingData.empty<SongListItem>()) },
playlists = remember { flowOf(PagingData.empty<Playlist>()) }.collectAsLazyPagingItems(), playlists = remember { flowOf(PagingData.empty<Playlist>()) },
coverArt = { _, _ -> null }, coverArt = { _, _ -> null },
onAlbumClick = {}, onAlbumClick = {},
onArtistClick = {}, onArtistClick = {},