🐕

JetpackCompose PagerとScrollableTabRowを組み合わせた際のバグと修正方法

2022/08/31に公開

環境

kotlin_version = 1.7.10
compose_version = 1.2.1
accompanist_version = 0.25.1

どうなってしまったか

https://google.github.io/accompanist/pager/#integration-with-tabs

こちらのサンプルとほぼ同じような感じで

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を参照するようにして

文字色も選択済みになるように変更した。

参考記事

https://developer.android.com/jetpack/compose/side-effects?hl=ja

https://zenn.dev/kaleidot725/books/jetpack-compose-sideeffect-samples/viewer/1-jc-side-effects

https://zenn.dev/kaleidot725/articles/2022-02-26-jc-snapshot-flow

https://ked4ma.medium.com/memo-flowのcollect-とcollectlatest-どっち使う問題-cdb3018bc8b5

Discussion