androidkotlinandroid-jetpack-composepull-to-refreshhorizontal-pager

Resolving scroll conflict: PullToRefresh (vertical) vs. Horizontal Pager in Jetpack Compose


I'm developing a screen in Jetpack Compose that combines two scrolling components: a PullToRefresh for vertical scrolling and an ImageSlider implemented using HorizontalPager for horizontal scrolling. The PullToRefresh is going to allow users to refresh the screen content by pulling down. However, I'm encountering an issue where the vertical scroll gesture for the PullToRefresh is not being detected. As a result, I'm unable to trigger the refresh operation by pulling down on the screen. The horizontal scrolling in the ImageSlider works as expected, but it seems to be interfering with the vertical scroll detection.

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PullToRefreshContent(
    isRefreshing: Boolean,
    onRefresh: () -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    val pullToRefreshState = rememberPullToRefreshState()
    
    Box(modifier = modifier.nestedScroll(nestedScrollConnection)) {
        content()
        
        // Refresh logic
        LaunchedEffect(isRefreshing) {
            if (isRefreshing) pullToRefreshState.startRefresh()
            else pullToRefreshState.endRefresh()
        }
        
        // PullToRefresh UI
        PullToRefreshContainer(
            state = pullToRefreshState,
            modifier = Modifier.align(Alignment.TopCenter)
        )
    }
}
@Composable
fun ImageSlider(images: List<Media?>) {
    val pagerState = rememberPagerState(initialPage = 0) { images.size }
    
    HorizontalPager(state = pagerState) { currentPage ->
        val painter = rememberAsyncImagePainter(model = images[currentPage]?.posterPath)
        Card {
            Image(painter = painter, contentDescription = null)
        }
    }
}

To resolve this conflict, I tried to implement a custom NestedScrollConnection to detect the scroll direction by examining the y offset of the scroll event. I assumed that a positive y offset would indicate a downward scroll. However, this solution was not working. When monitoring the scroll events, I observed that the y offset value is consistently either 0.0 or -0.0, regardless of the actual scroll direction.

val nestedScrollConnection = remember {
        object : NestedScrollConnection {
            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
                //Vertical scroll
                return if (available.y > 0) {
                    //Do refresh operation
                    onRefresh()
                    pullToRefreshState.nestedScrollConnection.onPreScroll(available, source)
                } 
                 //Horizontal scroll
                 else {
                    Offset.Zero
                }
            }
        }
    }

Solution

  • First, make sure that you are using the material3 pulltorefresh instead of the material2 pullrefresh. Also check that you are using at least

    implementation 'androidx.compose.material3:material3:1.3.0-beta05'
    

    I found that you can use a combination of verticalScroll and matchParentSize Modifiers to get the desired functionality.

    Please try out the following code:

    @Composable
    fun PullRefresh() {
    
        var isRefreshing by remember { mutableStateOf(false) }
        val state = rememberPullToRefreshState()
        val coroutineScope = rememberCoroutineScope()
        val onRefresh: () -> Unit = {
            isRefreshing = true
            coroutineScope.launch {
                delay(1000)
                isRefreshing = false
            }
        }
    
        Scaffold(
            topBar = {
                TopAppBar(
                    title = { Text("Title") },
                )
            }
        ) {
            PullToRefreshBox(
                modifier = Modifier.fillMaxSize().padding(it),
                state = state,
                isRefreshing = isRefreshing,
                onRefresh = onRefresh,
            ) {
                val pagerState = rememberPagerState { 3 }
    
                HorizontalPager(
                    modifier = Modifier
                        .verticalScroll(rememberScrollState())
                        .matchParentSize(),
                    state = pagerState
                ) { currentPage ->
                    Column(
                        modifier = Modifier.fillMaxSize(),
                        horizontalAlignment = Alignment.CenterHorizontally,
                        verticalArrangement = Arrangement.Center
                    ) {
                        Text(text = "PAGE $currentPage")
                    }
                }
            }
        }
    }
    

    The PullToRefreshBox Composable needs a vertically scrollable child Composable. We make the HorizontalPager vertically scrollable by applying the verticalScroll Modifier.

    However, with the verticalScroll Modifier, fillMaxSize is no longer working. Instead of fillMaxSize, we can use the fillParentSize Modifier that sets the size equal to the PullToRefreshBox Composable which provides a BoxScope.

    Output:

    Screenrecording