feat(android): swipe to change Library tabs
android / Build + lint + test (push) Has been cancelled

Operator: tabs in the Library view should feel swipeable, not just
tappable. Replace the selectedTab Int state + when-block content
with HorizontalPager whose state drives the PrimaryScrollableTabRow.
Tap routes through animateScrollToPage so swipe + tap share one
source of truth.

Horizontal pager gestures don't conflict with the LazyVerticalGrid
inside each tab (different axes) or with PullToRefreshScaffold's
vertical pull (different axes). HorizontalPager renders only the
current page by default; adjacent tabs remain composed during the
swipe but not eager-mounted at start.
This commit is contained in:
2026-06-02 10:04:38 -04:00
parent b83a6a4bdb
commit 9b3ec65476
@@ -4,7 +4,6 @@ package com.fabledsword.minstrel.library.ui
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -12,6 +11,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@@ -22,11 +23,10 @@ import androidx.compose.material3.Tab
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
@@ -69,8 +69,12 @@ fun LibraryScreen(
navController: NavHostController, navController: NavHostController,
viewModel: LibraryViewModel = hiltViewModel(), viewModel: LibraryViewModel = hiltViewModel(),
) { ) {
var selectedTab by remember { mutableIntStateOf(0) }
val tabs = LIBRARY_TABS val tabs = LIBRARY_TABS
// HorizontalPager owns the active-tab state — the TabRow reads
// pagerState.currentPage and animateScrollToPage drives it on tap,
// so swipe and tap stay in sync.
val pagerState = rememberPagerState(pageCount = { tabs.size })
val scope = rememberCoroutineScope()
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@@ -91,13 +95,13 @@ fun LibraryScreen(
}, },
) )
PrimaryScrollableTabRow( PrimaryScrollableTabRow(
selectedTabIndex = selectedTab, selectedTabIndex = pagerState.currentPage,
edgePadding = 16.dp, edgePadding = 16.dp,
) { ) {
tabs.forEachIndexed { index, label -> tabs.forEachIndexed { index, label ->
Tab( Tab(
selected = selectedTab == index, selected = pagerState.currentPage == index,
onClick = { selectedTab = index }, onClick = { scope.launch { pagerState.animateScrollToPage(index) } },
text = { Text(label) }, text = { Text(label) },
) )
} }
@@ -105,8 +109,11 @@ fun LibraryScreen(
} }
}, },
) { inner -> ) { inner ->
Box(modifier = Modifier.fillMaxSize().padding(inner)) { HorizontalPager(
when (selectedTab) { state = pagerState,
modifier = Modifier.fillMaxSize().padding(inner),
) { page ->
when (page) {
TAB_ARTISTS -> ArtistsTab(viewModel = viewModel, navController = navController) TAB_ARTISTS -> ArtistsTab(viewModel = viewModel, navController = navController)
TAB_ALBUMS -> AlbumsTab(viewModel = viewModel, navController = navController) TAB_ALBUMS -> AlbumsTab(viewModel = viewModel, navController = navController)
TAB_HISTORY -> HistoryTab( TAB_HISTORY -> HistoryTab(