start the library on the stored tab instead of scrolling to it

The pager's settled-page collector reports its initial value, so a cold start
wrote Albums back before the stored tab had been read - racing the very setting
this branch added, able to pin the app to Albums and overwrite the choice. The
screen waits for the stored tab before composing now, so the pager latches the
right page and the initial report matches it, which also drops the spurious
scroll and the search clear that came with it. The report is guarded anyway.

The data layer no longer names a UI tab from a coincidentally same-named enum:
it returns the raw stored value and the view model owns the Albums fallback.
This commit is contained in:
austinried
2026-10-04 00:30:04 +00:00
parent cf92604e3a
commit 92637deb48
5 changed files with 32 additions and 30 deletions
@@ -134,7 +134,7 @@ class UserPreferences(
private fun listQueryKey(tab: LibraryListTab) = stringPreferencesKey("list_query_${tab.key}")
fun libraryTab(): Flow<String> = store.data.map { prefs -> prefs[libraryTabKey] ?: LibraryListTab.Albums.name }
fun libraryTab(): Flow<String?> = store.data.map { prefs -> prefs[libraryTabKey] }
suspend fun setLibraryTab(tab: String) {
store.edit { prefs -> prefs[libraryTabKey] = tab }
@@ -151,6 +151,8 @@ data class SortOption(
fun LibraryTab.listTab(): LibraryListTab = LibraryListTab.valueOf(name)
internal fun libraryTabFor(name: String?): LibraryTab = LibraryTab.entries.firstOrNull { it.name == name } ?: LibraryTab.Albums
fun sortOptionsFor(tab: LibraryTab): List<SortOption> =
when (tab) {
LibraryTab.Albums -> {
@@ -195,8 +197,10 @@ fun LibraryRoute(
viewModel: LibraryViewModel = koinViewModel(),
playbackController: PlaybackController = koinInject(),
) {
val selectedTab by viewModel.selectedTab.collectAsStateWithLifecycle()
var previousTabName by rememberSaveable { mutableStateOf(selectedTab.name) }
// Wait for the stored tab before composing: the pager latches its initial page, so starting on
// the default and scrolling to the stored one would flash and, worse, report the default back.
val selectedTab = viewModel.selectedTab.collectAsStateWithLifecycle().value ?: return
var previousTabName by remember { mutableStateOf(selectedTab.name) }
val previousTab = LibraryTab.entries.firstOrNull { it.name == previousTabName } ?: LibraryTab.Albums
val syncing by viewModel.syncing.collectAsStateWithLifecycle()
val listTab = selectedTab.listTab()
@@ -241,7 +245,7 @@ fun LibraryRoute(
)
LibraryScreen(
selectedTab = selectedTab,
onTabSelected = { viewModel.selectTab(it) },
onTabSelected = { tab -> if (viewModel.selectedTab.value != tab) viewModel.selectTab(tab) },
syncing = syncing,
bottomInset = bottomInset,
artwork = artwork,
@@ -135,11 +135,11 @@ class LibraryViewModel(
fun listQuery(tab: LibraryListTab): StateFlow<ListQuery> = listQueries.getValue(tab)
val selectedTab: StateFlow<LibraryTab> =
val selectedTab: StateFlow<LibraryTab?> =
userPreferences
.libraryTab()
.map { name -> LibraryTab.entries.firstOrNull { it.name == name } ?: LibraryTab.Albums }
.stateIn(viewModelScope, SharingStarted.Eagerly, LibraryTab.Albums)
.map(::libraryTabFor)
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
fun selectTab(tab: LibraryTab) {
viewModelScope.launch { userPreferences.setLibraryTab(tab.name) }
@@ -10,6 +10,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import org.junit.runner.RunWith
import java.io.File
@@ -32,6 +33,7 @@ class UserPreferencesTest {
ListQuery("Added", descending = true, starred = StarredFilter.Starred, downloaded = true),
)
prefs.setListQuery(LibraryListTab.Artists, ListQuery("AlbumCount", descending = true))
prefs.setListQuery(LibraryListTab.Playlists, ListQuery("Updated", starred = StarredFilter.NotStarred))
assertEquals(
ListQuery("Added", descending = true, starred = StarredFilter.Starred, downloaded = true),
@@ -41,30 +43,10 @@ class UserPreferencesTest {
ListQuery("AlbumCount", descending = true),
prefs.listQuery(LibraryListTab.Artists).first(),
)
file.delete()
}
@Test
fun everyTabsListQueryIsKeptIndependently() =
runTest {
val file = File.createTempFile("user-prefs", ".preferences_pb").apply { delete() }
val store = PreferenceDataStoreFactory.create(scope = backgroundScope) { file }
val prefs = UserPreferences(store)
prefs.setListQuery(LibraryListTab.Albums, ListQuery("Added", descending = true, downloaded = true))
prefs.setListQuery(LibraryListTab.Artists, ListQuery("AlbumCount", starred = StarredFilter.NotStarred))
prefs.setListQuery(LibraryListTab.Playlists, ListQuery("Updated", descending = true))
assertEquals(
ListQuery("Added", descending = true, downloaded = true),
prefs.listQuery(LibraryListTab.Albums).first(),
ListQuery("Updated", starred = StarredFilter.NotStarred),
prefs.listQuery(LibraryListTab.Playlists).first(),
)
assertEquals(
ListQuery("AlbumCount", starred = StarredFilter.NotStarred),
prefs.listQuery(LibraryListTab.Artists).first(),
)
assertEquals(ListQuery("Updated", descending = true), prefs.listQuery(LibraryListTab.Playlists).first())
file.delete()
}
@@ -76,7 +58,7 @@ class UserPreferencesTest {
val store = PreferenceDataStoreFactory.create(scope = backgroundScope) { file }
val prefs = UserPreferences(store)
assertEquals("Albums", prefs.libraryTab().first())
assertNull(prefs.libraryTab().first())
prefs.setLibraryTab("Playlists")
@@ -0,0 +1,16 @@
package com.subtracks.ui.library
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class LibraryTabTest {
@Test
fun theStoredNameMapsToItsTabAndAnythingElseToAlbums() {
assertEquals(LibraryTab.Playlists, libraryTabFor("Playlists"))
assertEquals(LibraryTab.Albums, libraryTabFor(null))
assertEquals(LibraryTab.Albums, libraryTabFor("Books"))
}
}