Unit 4 Pathway 2 Activity 2: Navigate between screens with Compose¶

Before you begin¶

  • Up until this point, the apps you’ve worked on consisted of a single screen. However, a lot of the apps you use probably have multiple screens that you can navigate through.

  • In modern Android development, multiscreen apps are created using the Navigation Compose component. The Navigation Compose component allows you to easily build multi screen apps in Compose using a declarative approach, just like building user interfaces. This codelab introduces the essentials of the Navigation Compose component, how to make the AppBar responsive, and how to send data from your app to another app using intents—all while demonstrating best practices in an increasingly complex app.

Prerequisites¶

  • Familiarity with the Kotlin language, including function types, lambdas, and scope functions

  • Familiarity with basic Row and Column layouts in Compose

What you’ll learn¶

  • Create a NavHost composable to define routes and screens in your app.

  • Navigate between screens using a NavHostController.

  • Manipulate the back stack to navigate to previous screens.

  • Use intents to share data with another app.

  • Customize the AppBar, including the title and back button.

What you’ll build¶

  • Instead of building the app from the starter code, we’ll jump straight to the solution code and focus on learning how app architecture is implemented, using the solution code as an example.

What you’ll need¶

  • The latest version of Android Studio

  • An internet connection to download the code

Download solution code¶

App walkthrough¶

  • The Cupcake app is a bit different than the apps you’ve worked with so far. Instead of all the content displaying on a single screen, the app has four separate screens, and the user can navigate through each screen while ordering cupcakes.

Start order screen¶

  • The first screen presents the user with three buttons that correspond to the quantity of cupcakes to order.

  • In code, this is represented by the StartOrderScreen composable in StartOrderScreen.kt.

  • The screen consists of a single column, with an image and text, along with three custom buttons to order different quantities of cupcakes. The custom buttons are implemented by the SelectQuantityButton composable, which is also in StartOrderScreen.kt.

Choose flavor screen¶

  • After selecting the quantity, the app prompts the user to select a cupcake flavor. The app uses radio buttons to display different options. Users can select one flavor out of a choice of possible flavors.

  • The list of possible flavors is stored as a list of string resource IDs in data.DataSource.kt.

Choose pickup date screen¶

  • After choosing a flavor, the app presents users with another series of radio buttons to select a pickup date. Pickup options come from a list returned by the pickupOptions() function in OrderViewModel.

  • Both the Choose Flavor screen and Choose Pickup Date screen are represented by the same composable, SelectOptionScreen in SelectOptionScreen.kt. Why use the same composable? The layout of these screens is exactly the same! The only difference is the data, but you can use the same composable to display both the flavor and pickup date screens.

Order Summary screen¶

  • After selecting the pickup date, the app displays the Order Summary screen where the user can review and complete the order.

  • This screen is implemented by the OrderSummaryScreen composable in SummaryScreen.kt.

  • The layout consists of a Column containing all the information about their order, a Text composable for the subtotal, and buttons to either send the order to another app or cancel the order and return to the first screen.

  • If users choose to send the order to another app, the Cupcake app displays an Android ShareSheet that shows different sharing options.

    ../_images/unit4-pathway2-activity2-section3-13bde33712e135a4_1440.png
  • The current state of the app is stored in data/OrderUiState.kt. The OrderUiState data class contains properties to store the user’s selections from each screen.

  • The screens of the app will be presented in the CupcakeApp composable.

Reusable composables¶

  • Where appropriate, the sample apps in this course are designed to implement best practices. The Cupcake app is no exception. In the ui.components package, you’ll see a file named CommonUi.kt that contains a FormattedPriceLabel composable. Multiple screens in the app use this composable to format the order price consistently. Rather than duplicate the same Text composable with the same formatting and modifiers, you can define FormattedPriceLabel once and then reuse it as many times as needed for other screens.

  • The flavor and pickup date screens use the SelectOptionScreen composable, which is also reusable. This composable takes a parameter named options of the type List<String> that represents the options to display. The options appear in a Row, consisting of a RadioButton composable and a Text composable containing each string. A Column surrounds the entire layout and also contains a Text composable to show the formatted price, a Cancel button, and a Next button.

Define routes and create a NavHostController¶

Parts of the Navigation Component¶

  • The Navigation component has three main parts:

    • NavController: Responsible for navigating between destinations—that is, the screens in your app.

    • NavGraph: Maps composable destinations (screens) to navigate to.

    • NavHost: Composable acting as a container for displaying the current destination of the NavGraph.

Define routes for destinations¶

  • One of the fundamental concepts of navigation in a Compose app is the route. A route is a string that corresponds to a destination (a screen). This idea is similar to the concept of a URL. Just as a different URL maps to a different page on a website, a route is a string that maps to a destination and serves as its unique identifier.

  • A destination is typically a single Composable or group of Composables corresponding to what the user sees. The Cupcake app needs destinations for the start order screen, the flavor screen, the pickup date screen, and the order summary screen.

  • There are a finite number of screens in an app, so there are also a finite number of routes. You can define an app’s routes using an enum class. Enum classes in Kotlin have a name property that returns a string with the property name.

  • The four routes of the Cupcake app are defined in CupcakeScreen.kt:

    enum class CupcakeScreen() {
        Start,
        Flavor,
        Pickup,
        Summary,
    }
    
    • Start: Select the quantity of cupcakes from one of three buttons.

    • Flavor: Select the flavor from a list of choices.

    • Pickup: Select the pickup date from a list of choices.

    • Summary: Review the selections and either send or cancel the order.

A NavHost controller¶

  • A NavHost is a Composable that displays other composable destinations, based on a given route. For example, if the route is Flavor, then the NavHost would show the screen to choose the cupcake flavor. If the route is Summary, then the app displays the summary screen.

  • The syntax for NavHost:

    ../_images/unit4-pathway2-activity2-secion4-fae7688d6dd53de9_1920.png
  • Two notable parameters:

    • navController: An instance of the NavHostController class. You can use this object to navigate between screens, for example, by calling the navigate() method to navigate to another destination. You can obtain the NavHostController by calling rememberNavController() from a composable function.

    • startDestination: A string route defining the destination shown by default when the app first displays the NavHost. In the case of the Cupcake app, this should be the Start route.

  • In CupcakeScreen.kt, the CupcakeApp contains this NavHost:

    @Composable
    fun CupcakeApp(
        viewModel: OrderViewModel = viewModel(),
        navController: NavHostController = rememberNavController()
    ) {
        // ...
    
        Scaffold(
    
            // ...
    
        ) { innerPadding ->
            val uiState by viewModel.uiState.collectAsState()
    
            NavHost(
                navController = navController,
                startDestination = CupcakeScreen.Start.name,
                modifier = Modifier
                    .fillMaxSize()
                    .verticalScroll(rememberScrollState())
                    .padding(innerPadding)
            ) {
                composable(route = CupcakeScreen.Start.name) {
                    StartOrderScreen(
                        quantityOptions = DataSource.quantityOptions,
                        onNextButtonClicked = {
                            viewModel.setQuantity(it)
                            navController.navigate(CupcakeScreen.Flavor.name)
                        },
                        modifier = Modifier
                            .fillMaxSize()
                            .padding(dimensionResource(R.dimen.padding_medium))
                    )
                }
                composable(route = CupcakeScreen.Flavor.name) {
                    val context = LocalContext.current
                    SelectOptionScreen(
                        subtotal = uiState.price,
                        onNextButtonClicked = { navController.navigate(CupcakeScreen.Pickup.name) },
                        onCancelButtonClicked = {
                            cancelOrderAndNavigateToStart(viewModel, navController)
                        },
                        options = DataSource.flavors.map { id -> context.resources.getString(id) },
                        onSelectionChanged = { viewModel.setFlavor(it) },
                        modifier = Modifier.fillMaxHeight()
                    )
                }
                composable(route = CupcakeScreen.Pickup.name) {
                    SelectOptionScreen(
                        subtotal = uiState.price,
                        onNextButtonClicked = { navController.navigate(CupcakeScreen.Summary.name) },
                        onCancelButtonClicked = {
                            cancelOrderAndNavigateToStart(viewModel, navController)
                        },
                        options = uiState.pickupOptions,
                        onSelectionChanged = { viewModel.setDate(it) },
                        modifier = Modifier.fillMaxHeight()
                    )
                }
                composable(route = CupcakeScreen.Summary.name) {
                    val context = LocalContext.current
                    OrderSummaryScreen(
                        orderUiState = uiState,
                        onCancelButtonClicked = {
                            cancelOrderAndNavigateToStart(viewModel, navController)
                        },
                        onSendButtonClicked = { subject: String, summary: String ->
                            shareOrder(context, subject = subject, summary = summary)
                        },
                        modifier = Modifier.fillMaxHeight()
                    )
                }
            }
        }
    }
    

Handle routes¶

  • Like other composables, NavHost takes a function type for its content.

  • Within the content function of a NavHost, call the composable() function as many times as needed, once for each route.

  • Each composable() function has two required parameters.

    • route: A string corresponding to the name of a route. This can be any unique string. You’ll use the name property of the enum class CupcakeScreen() constants.

    • content: Here you can call a composable that you want to display for the given route.

  • It has similar feels to Flask and Spring Boot.

  • The route handling code:

    @Composable
    fun CupcakeApp(
        viewModel: OrderViewModel = viewModel(),
        navController: NavHostController = rememberNavController()
    ) {
        // ...
    
        Scaffold(
    
            // ...
    
        ) { innerPadding ->
            val uiState by viewModel.uiState.collectAsState()
    
            NavHost(
    
              // ...
    
            ) {
                composable(route = CupcakeScreen.Start.name) {
                    StartOrderScreen(
                        quantityOptions = DataSource.quantityOptions,
                        onNextButtonClicked = {
                            viewModel.setQuantity(it)
                            navController.navigate(CupcakeScreen.Flavor.name)
                        },
                        modifier = Modifier
                            .fillMaxSize()
                            .padding(dimensionResource(R.dimen.padding_medium))
                    )
                }
    
                composable(route = CupcakeScreen.Flavor.name) {
                    // Context is an abstract class provided by the Android system. It allows access to application-specific resources and classes. You can use this variable to get the strings from the list of resource IDs in the view model, to display the list of flavors.
                    val context = LocalContext.current
                    SelectOptionScreen(
                        subtotal = uiState.price,
                        onNextButtonClicked = { navController.navigate(CupcakeScreen.Pickup.name) },
                        onCancelButtonClicked = {
                            cancelOrderAndNavigateToStart(viewModel, navController)
                        },
                        options = DataSource.flavors.map { id -> context.resources.getString(id) },
                        onSelectionChanged = { viewModel.setFlavor(it) },
                        modifier = Modifier.fillMaxHeight()
                    )
                }
    
                composable(route = CupcakeScreen.Pickup.name) {
                    SelectOptionScreen(
                        subtotal = uiState.price,
                        onNextButtonClicked = { navController.navigate(CupcakeScreen.Summary.name) },
                        onCancelButtonClicked = {
                            cancelOrderAndNavigateToStart(viewModel, navController)
                        },
                        options = uiState.pickupOptions,
                        onSelectionChanged = { viewModel.setDate(it) },
                        modifier = Modifier.fillMaxHeight()
                    )
                }
    
                composable(route = CupcakeScreen.Summary.name) {
                    val context = LocalContext.current
                    OrderSummaryScreen(
                        orderUiState = uiState,
                        onCancelButtonClicked = {
                            cancelOrderAndNavigateToStart(viewModel, navController)
                        },
                        onSendButtonClicked = { subject: String, summary: String ->
                            shareOrder(context, subject = subject, summary = summary)
                        },
                        modifier = Modifier.fillMaxHeight()
                    )
                }
            }
        }
    }