🐕
JetpackCompose PagerとScrollableTabRowを組み合わせた際のバグと修正方法
環境
kotlin_version = 1.7.10
compose_version = 1.2.1
accompanist_version = 0.25.1
どうなってしまったか
こちらのサンプルとほぼ同じような感じで
Tabデータが複数あってその中にコンテンツがあり
コンテンツをタップすると詳細(なんでもいい)に遷移する画面です。
このコンテンツをタップして遷移し、
詳細から戻って再度ScrollableTabRowTestScreenに戻ってきた際、
(ScrollableTabRowTestScreen 遷移-> Detail,
Detail 戻る-> ScrollableTabRowTestScreen)
タブ自体は選択されているもののアニメーションが起こらず、
タブ位置が置いてけぼりになってしまいます(本当に困る)
サンプルコード
@OptIn(ExperimentalPagerApi::class)
@Composable
fun ScrollableTabRowTestScreen(
currentIndex: Int?,
listList: List<List>?,
onClickAction: () -> Unit
) {
currentIndex ?: return
listList ?: return
val pagerState = rememberPagerState(initialPage = currentIndex)
val coroutineScope = rememberCoroutineScope()
val list = listList[pagerState.currentPage]
Column(
modifier = Modifier
.fillMaxSize()
) {
ScrollableTabRow(
// Our selected tab is our current page
selectedTabIndex = pagerState.currentPage,
// Override the indicator, using the provided pagerTabIndicatorOffset modifier
indicator = { tabPositions ->
TabRowDefaults.Indicator(
Modifier.pagerTabIndicatorOffset(pagerState, tabPositions),
)
}
) {
// Add tabs for all of our pages
listList.forEachIndexed { index, list ->
Tab(
text = {
Text(
text = list.title ?: "",
)
},
selected = pagerState.currentPage == index,
onClick = {
coroutineScope.launch {
pagerState.animateScrollToPage(index)
}
},
modifier = Modifier.height(38.dp)
)
}
}
HorizontalPager(
count = listList.size,
modifier = Modifier
.fillMaxWidth(),
state = pagerState,
) {
Column(
Modifier.verticalScroll(rememberScrollState())
) {
list.list?.forEachIndexed { index, it ->
HogeWidget(
title = it.title ?: "",
onClickAction = {
onClickAction(
it.id ?: ""
)
}
)
}
}
}
}
}
原因
遷移後戻ってくる際にpagerState.currentPageが一瞬0になってしまうようで
アニメーションが走るときは0で行われた後に他のものが走るようで
タブが置いてけぼりになってしまうようです.......
修正後サンプルコード
@OptIn(ExperimentalPagerApi::class)
@Composable
fun ScrollableTabRowTestScreen(
currentIndex: Int?,
listList: List<List>?,
onClickAction: () -> Unit
) {
currentIndex ?: return
listList ?: return
val pagerState = rememberPagerState(initialPage = currentIndex)
val coroutineScope = rememberCoroutineScope()
val list = listList[pagerState.currentPage]
val currentPosition = rememberSaveable {
mutableStateOf<Int?>(currentIndex)
}
LaunchedEffect(pagerState) {
snapshotFlow { pagerState.currentPage }
.filter { it != 0 }
.collectLatest {
currentPosition.value = it
}
}
Column(
modifier = Modifier
.fillMaxSize()
) {
ScrollableTabRow(
// Our selected tab is our current page
selectedTabIndex = (currentPosition.value ?: 0),
// Override the indicator, using the provided pagerTabIndicatorOffset modifier
indicator = { tabPositions ->
TabRowDefaults.Indicator(
Modifier.pagerTabIndicatorOffset(pagerState, tabPositions),
)
}
) {
// Add tabs for all of our pages
listList.forEachIndexed { index, list ->
Tab(
text = {
Text(
text = list.title ?: "",
)
},
selected = if (currentPosition.value == pagerState.currentPage) currentPosition.value == index else pagerState.currentPage == index,
onClick = {
currentPosition.value = index
coroutineScope.launch {
pagerState.animateScrollToPage(index)
}
},
modifier = Modifier.height(38.dp)
)
}
}
HorizontalPager(
count = listList.size,
modifier = Modifier
.fillMaxWidth(),
state = pagerState,
) { idx ->
Column(
Modifier.verticalScroll(rememberScrollState())
) {
list.list?.forEachIndexed { index, it ->
HogeWidget(
title = it.title ?: "",
onClickAction = {
currentPosition.value = idx
onClickAction(
it.id ?: ""
)
}
)
}
}
}
}
}
解説
rememberSaveabledでcurrentPositionを新たに作成し、
LaunchedEffectでsnapshotFlowしてfilterをかけ
collectLatestで最新に更新するようにした。
しかしこれだけだとfilterをかけた関係で
一番最初のindex時に文字色が変わらなくなってしまうので
TabのselectedTabIndexを
currentPosition.value と pagerState.currentPage が
一致する場合のみcurrentPosition.valueを参照するようにして
文字色も選択済みになるように変更した。
参考記事
Discussion