CPS251 Android Development by Scott Shaper

Passing Arguments

What are Navigation Arguments?

Navigation arguments are like passing notes between screens. When you need to share information from one screen to another, you use arguments to carry that data along with the navigation. For example, when a user clicks on a product in a list, you need to pass the product ID to the detail screen to show the correct information.

Types of Arguments

Type What It's For Example Use
String Text data User names, messages
Int Whole numbers IDs, counts
Float Decimal numbers Prices, measurements
Boolean True/False values Settings, flags

Basic Argument Passing

@Composable
fun NavigationWithArgs() {
    val navController = rememberNavController()

    NavHost(
        navController = navController,
        startDestination = "home"
    ) {
        // Home screen route - starting point of the navigation
        composable("home") {
            HomeScreen(
                onNavigateToProfile = { data ->
                    // Navigate to profile screen with a userId parameter
                    // The route will be constructed as "enteredText/$data"
                    navController.navigate("enteredText/$data")
                }
            )
        }

        // Profile screen route with argument
        // The {data} in the route is a placeholder for the actual user ID
        composable(
            route = "enteredText/{data}",
            // Define the argument type as String
            arguments = listOf(
                navArgument("data") { type = NavType.StringType }
            )
        ) { backStackEntry ->
            // Extract the data argument from the navigation back stack entry
            val data = backStackEntry.arguments?.getString("data")
            ProfileScreen(
                data = data,
                onNavigateBack = {
                    navController.popBackStack()
                }
            )
        }
    }
}

/**
 * HomeScreen displays the main screen of the application.
 * It contains a button that navigates to a specific user's profile.
 *
 * @param onNavigateToProfile Callback function that takes a userId parameter
 *                          and handles navigation to the profile screen
 */
@Composable
fun HomeScreen(onNavigateToProfile: (String) -> Unit) {
    var text by remember { mutableStateOf("") }
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(top = 50.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Top
    ) {
        OutlinedTextField(
            value = text,
            onValueChange = {text = it},
            label = { Text("Enter text")}
        )
        // Button that triggers navigation with a hardcoded user ID
        // In a real app, this would typically come from a user selection or authentication
        Button(onClick = { onNavigateToProfile(text) }) {
            Text("Send text to profile screen")
        }
    }
}

/**
 * ProfileScreen displays the profile information for a specific user.
 * It shows the content the user passed through navigation and provides a way to go back.
 *
 * @param data The data the user entered.
 * @param onNavigateBack Callback function to handle navigation back to the home screen
 */
@Composable
fun ProfileScreen(
    data: String?,
    onNavigateBack: () -> Unit
) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(top = 50.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Top
    ) {
        // Display the user ID passed through navigation
        Text("Entered Text: $data")
        Button(onClick = onNavigateBack) {
            Text("Go Back")
        }
    }
}

Understanding the Example Step by Step:

How Route Matching Works

The navigation system uses a pattern-matching system to determine which composable to show:

  • When you call navController.navigate("enteredText/$data"), the system looks for a matching route pattern
  • The pattern "enteredText/{data}" in the composable definition acts like a template:
    • enteredText/ must match exactly
    • {data} is a placeholder that matches any string
  • When a match is found, the system:
    • Extracts as the value for data
    • Executes the lambda in the matching composable definition
    • Shows the ProfileScreen with the extracted data
Understanding backStackEntry

The name backStackEntry can be confusing, but it's not about going back - it's about the current navigation state:

  • backStackEntry represents the current destination in the navigation stack
  • It's called "backStack" because Android maintains a stack of screens you've visited:
    • When you navigate to a new screen, it's added to the top of the stack
    • When you go back, the top screen is removed from the stack
    • The backStackEntry gives you access to the current screen's information
  • In our example:
    composable(
                route = "enteredText/{data}",
                // Define the argument type as String
                arguments = listOf(
                    navArgument("data") { type = NavType.StringType }
                )
            ) { backStackEntry ->
                // Extract the data argument from the navigation back stack entry
                val data = backStackEntry.arguments?.getString("data")
                ProfileScreen(
                    data = data,
                    onNavigateBack = {
                        navController.popBackStack()
                    }
                )
            }
  • Think of backStackEntry as "the current navigation state" rather than "going back"
  • It's just a parameter name - we could rename it to currentDestination or navEntry if we wanted
Mapping Routes to Composables

Important: The connection between the route name and the composable function is explicitly defined in the NavHost:

composable(
            route = "enteredText/{data}",//this is just a string we made up
            // Define the argument type as String
            arguments = listOf(
                navArgument("data") { type = NavType.StringType }
            )
        ) { backStackEntry ->
            // Extract the data argument from the navigation back stack entry
            val data = backStackEntry.arguments?.getString("data")
            ProfileScreen(
                data = data,
                onNavigateBack = {
                    navController.popBackStack()
                }
            )
        }
  • The route name "enteredText" is just a string we chose - it could be "userText" or "userData" or anything else
  • We explicitly tell the navigation system to show ProfileScreen in the lambda after the route pattern
  • There's no automatic connection between the route name and the composable name - we create this connection in our code
  • This is why we could rename the route to "user/{userText}" and it would still show ProfileScreen, as long as we update the navigation call navController.navigate("user/$userText") to match.
1. Setting Up Navigation

The NavigationWithArgs composable sets up our navigation structure:

  • rememberNavController() creates a controller to manage navigation
  • NavHost defines our navigation graph with "home" as the starting point
  • Two destinations are defined: "home" and "ProfileScreen"
2. Defining the Route with Arguments

In the ProfileScreen route, notice how we define the argument:

  • "enteredText/{data}" - The curly braces {data} indicate a dynamic argument
  • navArgument("data") - Defines the argument and its type (StringType)
  • This tells the navigation system to expect a string value for data
3. Passing the Argument

In the HomeScreen, when the button is clicked:

  • onNavigateToProfile(text) is called
  • This triggers navController.navigate("enteredText/$data")
  • The $data is replaced with the actual text entered, making the full route "enteredText/yourTextHere"
4. Receiving the Argument

In the ProfileScreen destination:

  • backStackEntry.arguments?.getString("data") retrieves the passed argument
  • The ?. operator safely handles the case where arguments might be null
  • The retrieved data is then passed to ProfileScreen as a parameter
5. Using the Argument

In ProfileScreen:

  • The data parameter is used to display the passed text
  • Note that data is nullable (String?) for safety
  • The back button uses popBackStack() to return to the previous screen

Key Points:

  • Arguments are defined in the route string with {argumentName}
  • Each argument needs a type definition
  • Arguments are accessed from backStackEntry
  • Always handle nullable arguments safely

How this example renders

Above is just a snippet of the code to view the full code, you need to go to my GitHub page and look at the chapter9 passingArgs.kt file.

Basic Example Basic Example

Multiple Arguments

@Composable
    fun NavigationWithMultipleArgs() {
        val navController = rememberNavController()
        
        NavHost(
            navController = navController,
            startDestination = "home"
        ) {
            composable("home") {
                HomeScreen(
                    onNavigateToProduct = { id, name -> 
                        navController.navigate("product/$id/$name") 
                    }
                )
            }
            composable(
                "product/{id}/{name}",
                arguments = listOf(
                    navArgument("id") { type = NavType.IntType },
                    navArgument("name") { type = NavType.StringType }
                )
            ) { backStackEntry ->
                val id = backStackEntry.arguments?.getInt("id")
                val name = backStackEntry.arguments?.getString("name")
                ProductScreen(
                    id = id,
                    name = name,
                    onNavigateBack = {
                        navController.popBackStack()
                    }
                )
            }
        }
    }
   
    @Composable
    fun HomeScreen(onNavigateToProduct: (Int, String) -> Unit) {
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(16.dp)
                .padding(top = 50.dp),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.spacedBy(8.dp)
        ) {
            Text(
                text = "Available Products",
                modifier = Modifier.padding(bottom = 16.dp)
            )
            
            // Sample product list - in a real app, this would come from a data source
            val products = listOf(
                Product(1, "Smartphone"),
                Product(2, "Laptop"),
                Product(3, "Headphones"),
                Product(4, "Tablet")
            )
            
            // Display each product as a button
            products.forEach { product ->
                Button(
                    onClick = { onNavigateToProduct(product.id, product.name) },
                    modifier = Modifier.fillMaxWidth()
                ) {
                    Text("View ${product.name}")
                }
            }
        }
    }
    
   
    @Composable
    fun ProductScreen(
        id: Int?,
        name: String?,
        onNavigateBack: () -> Unit
    ) {
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(16.dp)
                .padding(top = 50.dp),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            // Display product details
            Text(
                text = "Product Details",
                modifier = Modifier.padding(bottom = 8.dp)
            )
            
            // Show product information if available
            if (id != null && name != null) {
                Text("Product ID: $id")
                Text("Product Name: $name")
                
                // Additional product details could be added here
                // For example: description, price, specifications, etc.
            } else {
                Text("Error: Product information not available")
            }
            
            // Back button
            Button(
                onClick = onNavigateBack,
                modifier = Modifier.padding(top = 16.dp)
            ) {
                Text("Back to Products")
            }
        }
    }
    
    data class Product(
        val id: Int,
        val name: String
    )
    
    
    @Composable
    fun ProfileScreen(
        userId: String?,
        onNavigateBack: () -> Unit
    ) {
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(top = 50.dp),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Top
        ) {
            // Display the user ID passed through navigation
            Text("Profile for user: $userId")
            Button(onClick = onNavigateBack) {
                Text("Go Back")
            }
        }
    }
    

Understanding the Multiple Arguments Example

1. Data Structure

First, we define a simple data class to represent our products:

  • data class Product(val id: Int, val name: String) creates a template for our product data
  • This makes it easy to create and manage product information
  • In a real app, this data would typically come from a database or API
2. Home Screen with Product List

The HomeScreen composable shows a list of products:

  • Creates a sample list of products using our Product data class
  • Uses Column with Arrangement.spacedBy(8.dp) to space items evenly
  • Each product is displayed as a button using forEach
  • When a product button is clicked, it calls onNavigateToProduct(product.id, product.name)
  • This passes both the ID and name to the navigation system
3. Product Screen with Multiple Arguments

The ProductScreen composable receives and displays both arguments:

  • Takes both id: Int? and name: String? as parameters
  • Uses null safety checks (if (id != null && name != null)) to handle missing data
  • Displays the product information in a clean, organized layout
  • Includes a back button that uses onNavigateBack to return to the product list
4. Navigation Setup

The navigation is set up to handle multiple arguments:

  • Route pattern is "product/{id}/{name}" - defines two parameters in the URL
  • Both arguments are defined in the navArgument list:
    • id is defined as NavType.IntType
    • name is defined as NavType.StringType
  • When navigating, both values are passed in the URL: "product/1/Smartphone"
5. Key Differences from Single Argument Example
  • Multiple arguments are passed in the URL path, separated by slashes
  • Each argument needs its own type definition in the navArgument list
  • The receiving composable needs to handle multiple nullable parameters
  • Navigation calls need to provide all required arguments in the correct order

How this example renders

Above is just a snippet of the code to view the full code, you need to go to my GitHub page and look at the chapter9 multiple.kt file.

Multiple Example Multiple Example

Optional Arguments

composable(
    "profile/{userId}?showDetails={showDetails}",
    arguments = listOf(
        navArgument("userId") { type = NavType.StringType },
        navArgument("showDetails") { 
            type = NavType.BoolType
            defaultValue = false
        }
    )
) { backStackEntry ->
    val userId = backStackEntry.arguments?.getString("userId")
    val showDetails = backStackEntry.arguments?.getBoolean("showDetails") ?: false
    ProfileScreen(
        userId = userId,
        showDetails = showDetails,
        onNavigateBack = {
            navController.popBackStack()
        }
    )
}

Understanding Optional Arguments

1. How Optional Arguments Work

In the example, we define an optional argument using a special syntax in the route pattern:

  • "profile/{userId}?showDetails={showDetails}" - The ? after userId indicates that showDetails is optional
  • This means you can navigate to this screen in two ways:
    • With the optional argument: "profile/user123?showDetails=true"
    • Without the optional argument: "profile/user123"
2. Defining Optional Arguments

In the navArgument list, we specify that showDetails is optional:

navArgument("showDetails") { 
    type = NavType.BoolType
    defaultValue = false  // This makes it optional
}
  • The defaultValue property is what makes the argument optional
  • If the argument isn't provided in the URL, it will use this default value
  • In this case, if showDetails isn't specified, it defaults to false
3. Using Optional Arguments

When navigating to the screen, you have flexibility in how you pass the arguments:

// Navigate with the optional argument
navController.navigate("profile/user123?showDetails=true")

// Navigate without the optional argument
navController.navigate("profile/user123")  // showDetails will be false
4. Receiving Optional Arguments

In the composable, you can safely access the optional argument:

val showDetails = backStackEntry.arguments?.getBoolean("showDetails") ?: false
  • The ?: false is a fallback in case the argument is null
  • This ensures showDetails always has a value
  • You can then use this value to conditionally show content:
    if (showDetails) {
        // Show additional profile details
    } else {
        // Show basic profile information
    }
5. Common Use Cases for Optional Arguments
  • Feature flags or toggles (like showing detailed vs. basic views)
  • Filtering or sorting options
  • Display preferences (like dark mode or list/grid view)
  • Any parameter that isn't always needed but provides additional functionality when present
6. Best Practices
  • Always provide sensible default values for optional arguments
  • Use optional arguments sparingly - too many can make navigation confusing
  • Document which arguments are optional in your code comments
  • Consider using type-safe arguments for complex optional parameters

Tips for Success

  • Use meaningful argument names
  • Always handle nullable arguments
  • Consider using type-safe arguments
  • Keep argument names consistent

Common Mistakes to Avoid

  • Forgetting to define argument types
  • Not handling nullable arguments
  • Using wrong argument types
  • Forgetting to pass required arguments

Navigation vs State Management

It's important to understand the difference between passing arguments through navigation and managing state:

  • Navigation Arguments:
    • Used to pass data when moving between screens
    • Data is passed once during navigation
    • Arguments don't automatically update if they change
    • Good for initial screen setup or one-time data passing
  • State Management (covered in Chapter 10):
    • Used to maintain data that needs to stay in sync
    • Data can be updated and shared between screens
    • Changes are reflected across all screens automatically
    • Good for data that needs to be shared and updated

Example: If you're passing a user ID to a profile screen, use navigation arguments. If you need to share and update user preferences across multiple screens, use state management (which you'll learn about in Chapter 10).