androidkotlinandroid-jetpack-composeandroid-jetpack-navigationhorizontal-pager

how to handle Back Navigation Logic for Horizontal View Pager with Nested Sub-Screens in Jetpack Compose?


How to write a logic for Horizontal view pager screens I have 4 main screen and in this 4 screen each screen has multiple screen or view depends how logic fits because there is a also back button in which all screen back navigation will need to handle.

Back button is in topBar which I take as a common.

I'm building an Android app using Jetpack Compose, where I have a horizontal view pager (HorizontalPager) with 4 main screens and a pager indicator to show the user's current position. Each main screen has its own set of sub-screens, structured as follows:

  1. First Screen: 0 sub-screens
  2. Second Screen: 4 sub-screens
  3. Third Screen: 2 sub-screens
  4. Fourth Screen: 0 sub-screens

Requirements:

  1. Back Navigation Handling - I have a back button in the top bar (common for all screens) that should handle navigation in two ways:
  1. Pager Indicator - The pager indicator should accurately reflect the current main screen.

I'm looking for the best approach to handle this type of nested navigation in Jetpack Compose, specifically on managing back stacks within each main screen and ensuring smooth transitions across sub-screens and main screens in the view pager.

@Composable
fun AddDeviceFlow() {
   
    Scaffold(modifier = Modifier
        .fillMaxSize()
        .statusBarsPadding(),
        topBar = {
            AddDeviceAppBar(modifier = Modifier,
                showCloseButton = true,
                showBackButton = false,
                onClickBackButton = {
                    // handle viewpager screens and nested screens pop back navigation
                },
                onClickCloseButton = {})
        }
    ) { innerPadding ->
        Surface(modifier = Modifier.padding(innerPadding)) {
            Box(
                modifier = Modifier
                    .fillMaxSize()
                    .padding(16.dp)
            ) {
                HorizontalPagerWithGradientIndicators()
            }
        }

    }
}

@Composable
fun HorizontalPagerWithGradientIndicators() {
    val pagerState = rememberPagerState(pageCount = { 4 })
    val isLastPage = pagerState.currentPage == pagerState.pageCount - 1
    val coroutineScope = rememberCoroutineScope()

    val constraintSet = ConstraintSet {
        val (
            pager,
            pagerIndicator
        ) = createRefsFor(
            "pager",
            "pagerIndicator"
        )

        constrain(pagerIndicator) {
            top.linkTo(parent.top)
            bottom.linkTo(pager.top)
            centerHorizontallyTo(parent)
        }

        constrain(pager) {
            top.linkTo(pagerIndicator.bottom)
            bottom.linkTo(parent.bottom)
            centerHorizontallyTo(parent)
        }

    }
    ConstraintLayout(
        constraintSet = constraintSet,
        modifier = Modifier
    ) {
        // here is stepper code I removed for brevity  

        HorizontalPager(
            state = pagerState,
            modifier = Modifier
                .fillMaxWidth()
                .layoutId("pager"),
        ) {


        }

    }
}

Solution

  • This is how I handle Back Navigation

    @Composable
    fun AddDeviceFlow(onDeviceBackPressed:(Boolean) -> Unit) {
        val keyboardController = LocalSoftwareKeyboardController.current
        val context = LocalContext.current
        val pagerState = rememberPagerState(pageCount = { 4 })
        val coroutineScope = rememberCoroutineScope()
        var currentSubScreen by remember { mutableStateOf(SubScreenType.DEVICE_SOFTWARE) }
    
        BackHandler(enabled = currentSubScreen == SubScreenType.DEVICE_HARDWARE) {
            if (currentSubScreen == SubScreenType.DEVICE_HARDWARE) {
                currentSubScreen = SubScreenType.DEVICE_SOFTWARE
            } else {
                // Handle other cases or default back navigation
            }
        }
    
        Scaffold(
            modifier = Modifier
                .fillMaxSize()
                .statusBarsPadding(),
            topBar = {
                AddDeviceAppBar(
                    modifier = Modifier,
                    showCloseButton = true,
                    showBackButton = currentSubScreen == SubScreenType.DEVICE_HARDWARE,
                    onClickBackButton = {
                        handleBackNavigation(
                            pagerState = pagerState,
                            currentSubScreen = currentSubScreen,
                            setSubScreen = { newScreen -> currentSubScreen = newScreen },
                            coroutineScope = coroutineScope
                        )
                    },
                    onClickCloseButton = {
                        onDeviceBackPressed(true)
                    }
                )
            }
        ) { innerPadding ->
    
            Surface(modifier = Modifier.padding(innerPadding)) {
                Box(
                    modifier = Modifier
                        .fillMaxSize()
                        .padding(start = 16.dp, top = 16.dp, end = 16.dp)
                ) {
                    HorizontalPagerWithGradientIndicators(
                        pagerState = pagerState,
                        currentSubScreen = currentSubScreen,
                        setSubScreen = { newScreen -> currentSubScreen = newScreen }
                    )
                }
            }
        }
    }
    
    @Composable
    fun HorizontalPagerWithGradientIndicators(
        pagerState: PagerState,
        currentSubScreen: SubScreenType,
        setSubScreen: (SubScreenType) -> Unit
    ) {
        val coroutineScope = rememberCoroutineScope()
        val isLastPage = pagerState.currentPage == pagerState.pageCount - 1
    
        val constraintSet = ConstraintSet {
            val (
                pager,
                pagerIndicator
            ) = createRefsFor(
                "pager",
                "pagerIndicator"
            )
    
            constrain(pagerIndicator) {
                top.linkTo(parent.top)
                bottom.linkTo(pager.top)
                centerHorizontallyTo(parent)
            }
    
            constrain(pager) {
                top.linkTo(pagerIndicator.bottom)
                bottom.linkTo(parent.bottom)
                centerHorizontallyTo(parent)
            }
    
        }
        ConstraintLayout(
            constraintSet = constraintSet,
            modifier = Modifier
        ) {
            HorizontalPager(
                state = pagerState,
                userScrollEnabled = false,
                modifier = Modifier
                    .padding(top = 22.dp)
                    .layoutId("pager"),
            ) { page ->
                when (page) {
                    0 -> AddDeviceScreen {
                        coroutineScope.launch {
                            pagerState.animateScrollToPage(page + 1, animationSpec = tween(512))
                        }
                    }
                    1 -> {
                        when (currentSubScreen) {
                            SubScreenType.DEVICE_SOFTWARE -> ChooseSoftwareScreen(
                                onSoftWareSelected = { setSubScreen(SubScreenType.DEVICE_HARDWARE) }
                            )
                            SubScreenType.DEVICE_HARDWARE -> ChooseHardwareScreen(
                                onHardWareSelected = {
    
                                }
                            )
    
                        }
                    }
                }
            }
        }
    }
    
    // Helper function for back navigation logic
    private fun handleBackNavigation(
        pagerState: PagerState,
        currentSubScreen: SubScreenType,
        setSubScreen: (SubScreenType) -> Unit,
        coroutineScope: CoroutineScope
    ) {
        when (currentSubScreen) {
            SubScreenType.DEVICE_HARDWARE -> setSubScreen(SubScreenType.DEVICE_HARDWARE)
            SubScreenType.DEVICE_HARDWARE -> {
                if (pagerState.currentPage > 0) {
                    coroutineScope.launch {
                        pagerState.animateScrollToPage(pagerState.currentPage - 1)
                    }
                } else {
                    // Handle case where the user is at the first page and wants to go back
                }
            }
        }
    }
    
    
    
    
    enum class SubScreenType {
        DEVICE_HARDWARE,
        DEVICE_SOFTWARE
    }