androidkotlinandroid-jetpack-composejetpack-compose-navigation

How to navigate to the same composable but use different parameters?


I would like to navigate to different pages following the button I click on.

If I click on Easy button I want to call AdditionScreen with argument level = "easy". If I click on Medium button I want to go to AdditionScreen with argument level = "medium". I tried to use NavHost(). I am stuck with the ChooseLevel function

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController

enum class CalculationScreens {
    ChooseLevel,
    Addition
}

@Composable
fun ActivityChoice(
    navController: NavHostController = rememberNavController()
) {
    Scaffold() { innerPadding ->
        NavHost(
            navController = navController,
            startDestination = CalculationScreens.ChooseLevel.name
        ) {
            composable(route = CalculationScreens.ChooseLevel.name) {
                ChooseLevel(
                    onButtonLevelClicked = {
                        navController.navigate(CalculationScreens.Addition.name)
                    },
                    modifier = Modifier
                        .fillMaxSize()
                        .padding(innerPadding)
                )
            }

            composable(route = CalculationScreens.Addition.name) {
                AdditionScreen(
                    level = "easy",
                    modifier = Modifier
                        .fillMaxSize()
                        .padding(innerPadding)
                )
            }

            composable(route = CalculationScreens.Addition.name) {
                AdditionScreen(
                    level = "medium",
                    modifier = Modifier
                        .fillMaxSize()
                        .padding(innerPadding)
                )
            }
        }
    }
}

The page with the the two buttons:

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun ChooseLevel(
    onButtonLevelClicked: () -> Unit,
    modifier: Modifier = Modifier
) {
    Column(
        modifier = modifier,
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Button(
            onClick = onButtonLevelClicked,
            Modifier.widthIn(min = 250.dp)
        ) {
            Text("Easy")
        }
        Button(
            onClick = onButtonLevelClicked,
            Modifier.widthIn(min = 250.dp)
        ) {
            Text("Medium")
        }
    }
}

Is it possible to pass a variable (level in my case) when I click on a button? I think I have to modify the onClick call, but I don't know how.


Solution

  • You need to provide separate callbacks to ChooseLevel; one for easy, one for medium:

    @Composable
    fun ChooseLevel(
        onButtonLevelEasyClicked: () -> Unit,
        onButtonLevelMediumClicked: () -> Unit,
        modifier: Modifier = Modifier,
    ) {
        Column(
            modifier = modifier,
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center,
        ) {
            Button(
                onClick = onButtonLevelEasyClicked,
                Modifier.widthIn(min = 250.dp),
            ) {
                Text("Easy")
            }
            Button(
                onClick = onButtonLevelMediumClicked,
                Modifier.widthIn(min = 250.dp),
            ) {
                Text("Medium")
            }
        }
    }
    

    In your NavHost, where you call ChooseLevel, you must now provide the second callback. You have basically two options to do that:

    1. Split your single CalculationScreens.Addition into two separate screens:

      enum class CalculationScreens {
          ChooseLevel,
          AdditionEasy,
          AdditionMedium,
      }
      

      Then you can call ChooseLevel like this:

      ChooseLevel(
          onButtonLevelEasyClicked = {
              navController.navigate(CalculationScreens.AdditionEasy.name)
          },
          onButtonLevelMediumClicked = {
              navController.navigate(CalculationScreens.AdditionMedium.name)
          },
          modifier = ...,
      )
      

      Make sure the actual destinations you define in your NavHost by composable(...) use the proper routes.

    2. Only use a single Addition route, but supply it with a parameter. How that works is explained in detail in the documentation about Type safety in Kotlin DSL and Navigation Compose. Please note that you need to add the Kotlin Serialization plugin to your gradle setup.

      First, you need to replace your CalculationScreens enum with this:

      @Serializable
      private data object ChooseLevelRoute
      
      @Serializable
      private data class AdditionRoute(val level: String)
      

      As you can see, AdditionRoute has now a parameter (ChooseLevelRoute has not, why it is declared as a data object instead of a data class).

      In the NavHost, you do not declare the routes as Strings anymore, instead you declare them to be of the specific type directly (note the angle brackets):

      NavHost(
          navController = navController,
          startDestination = ChooseLevelRoute,
      ) {
          composable<ChooseLevelRoute> {
              ChooseLevel(
                  onButtonLevelEasyClicked = {
                      navController.navigate(AdditionRoute("easy"))
                  },
                  onButtonLevelMediumClicked = {
                      navController.navigate(AdditionRoute("medium"))
                  },
                  modifier = ...,
              )
          }
      
          composable<AdditionRoute> {
              val additionRoute = it.toRoute<AdditionRoute>()
              AdditionScreen(
                  level = additionRoute.level,
                  modifier = ...,
              )
          }
      }
      

      When navigating to a destination you need to provide one of the routes you declared earlier. In this case Addition("easy") and Addition("medium"), where you can now directly supply the parameter as needed.

      In composable<AdditionRoute> you can then access the route object by calling toRoute() on the supplied NavBackStackEntry, so you can pass its level parameter to the AdditionScreen composable.

    Although it requires a little bit more work to set up, I recommend the second approach since it is cleaner and scales better.


    Unrelated, but noteworthy:

    The innerPadding is expected to be applied to the first child of the Scaffold. That is NavHost, not your screens.