Navigation and the back stack

1. Before you begin

In this codelab, you'll finish implementing the rest of the Cupcake app, which you started in a previous codelab. The Cupcake app has multiple screens and shows an order flow for cupcakes. The completed app should allow the user to navigate through the app to:

  • Create a cupcake order
  • Use Up or Back button to go to a previous step of the order flow
  • Cancel an order
  • Send the order to another app such as an email app

Along the way, you'll learn about how Android handles tasks and the back stack for an app. This will allow you to manipulate the back stack in scenarios like canceling an order, which brings the user back to the first screen of the app (as opposed to the previous screen of the order flow).

Prerequisites

  • Able to create and use a shared view model across fragments in an activity
  • Familiar with using the Jetpack Navigation component
  • Have used data binding with LiveData to keep the UI in sync with the view model
  • Can build an intent to start a new activity

What you'll learn

  • How navigation affects the back stack of an app
  • How to implement custom back stack behavior

What you'll build

  • A cupcake ordering app that allows the user to send the order to another app and allows for canceling an order

What you need

  • A computer with Android Studio installed.
  • Code for the Cupcake app from completing the previous codelab

2. Starter app overview

This codelab uses the Cupcake app from the previous codelab. You can either use your code from completing that earlier codelab or download the starter code from GitHub.

Download the starter code for this codelab

If you download the starter code from GitHub, note that the folder name of the project is android-basics-kotlin-cupcake-app-viewmodel. Select this folder when you open the project in Android Studio.

To get the code for this codelab and open it in Android Studio, do the following.

Get the code

  1. Click on the provided URL. This opens the GitHub page for the project in a browser.
  2. On the GitHub page for the project, click the Code button, which brings up a dialog.

5b0a76c50478a73f.png

  1. In the dialog, click the Download ZIP button to save the project to your computer. Wait for the download to complete.
  2. Locate the file on your computer (likely in the Downloads folder).
  3. Double-click the ZIP file to unpack it. This creates a new folder that contains the project files.

Open the project in Android Studio

  1. Start Android Studio.
  2. In the Welcome to Android Studio window, click Open an existing Android Studio project.

36cc44fcf0f89a1d.png

Note: If Android Studio is already open, instead, select the File > New > Import Project menu option.

21f3eec988dcfbe9.png

  1. In the Import Project dialog, navigate to where the unzipped project folder is located (likely in your Downloads folder).
  2. Double-click on that project folder.
  3. Wait for Android Studio to open the project.
  4. Click the Run button 11c34fc5e516fb1c.png to build and run the app. Make sure it builds as expected.
  5. Browse the project files in the Project tool window to see how the app is set-up.

Now run the app and it should look like this.

45844688c0dc69a2.png

In this codelab, you will first finish implementing the Up button in the app, so that tapping it brings the user to the previous step of the order flow.

fbdc1793f9fea6da.png

Then you will be adding a Cancel button so that the user can cancel the order if they change their mind during the ordering process.

d66fdafeac1b0dcf.gif

Then you'll extend the app so that tapping Send Order to Another App shares the order with another app. Then the order can be sent to a cupcake shop via email for example.

170d76b64ce78f56.png

Let's dive in and complete the Cupcake app!

3. Implement Up button behavior

In the Cupcake app, the app bar shows an arrow to return to the previous screen. This is known as the Up button, and you've learned in previous codelabs. The Up button currently doesn't do anything, so fix this navigation bug in the app first.

fbdc1793f9fea6da.png

  1. In the MainActivity, you should already have code to set up the app bar (also known as action bar) with the nav controller. Make navController a class variable so you can use it in another method.
class MainActivity : AppCompatActivity(R.layout.activity_main) {

    private lateinit var navController: NavController

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val navHostFragment = supportFragmentManager
                .findFragmentById(R.id.nav_host_fragment) as NavHostFragment
        navController = navHostFragment.navController

        setupActionBarWithNavController(navController)
    }
}
  1. Within the same class, add code to override the onSupportNavigateUp() function. This code will ask the navController to handle navigating up in the app. Otherwise, fall back to back to the superclass implementation (in AppCompatActivity) of handling the Up button.
override fun onSupportNavigateUp(): Boolean {
   return navController.navigateUp() || super.onSupportNavigateUp()
}
  1. Run the app. The Up button should now work from the FlavorFragment, PickupFragment, and SummaryFragment. As you navigate to previous steps in the order flow, the fragments should show the right flavor and pickup date from the view model.

4. Learn about tasks and back stack

Now you're going to introduce a Cancel button within the order flow of your app. Cancelling an order at any point in the order process sends the user back to the StartFragment. To handle this behavior, you'll learn about tasks and back stack in Android.

Tasks

Activities in Android exist within tasks. When you open an app for the first time from the launcher icon, Android creates a new task with your main activity. A task is a collection of activities that the user interacts with when performing a certain job (i.e. checking email, creating a cupcake order, taking a photo).

Activities are arranged in a stack, known as a back stack, where each new activity the user visits gets pushed onto the back stack for the task. You can think of it as a stack of pancakes, where each new pancake is added on top of the stack. The activity on the top of the stack is the current activity the user is interacting with. The activities below it on the stack have been put in the background and have been stopped.

517054e483795b46.png

The back stack is useful for when the user wants to navigate backwards. Android can remove the current activity from the top of the stack, destroy it, and start the activity underneath it again. It's known as popping an activity off the stack, and bringing the previous activity to the foreground for the user to interact with. If the user wants to go back multiple times, Android will keep popping the activities off the top of the stack until you get closer to the bottom of the stack. When there are no more activities in the backstack, the user is brought back to the launcher screen of the device (or to the app that launched this one).

Let's look at the version of the Words app that you implemented with 2 activities: MainActivity and DetailActivity.

When you first launch the app, the MainActivity opens and is added to the task's back stack.

4bc8f5aff4d5ee7f.png

When you click on a letter, the DetailActivity is launched and pushed onto the backstack. This means the DetailActivity has been created, started, and resumed so the user can interact with it. The MainActivity is put into the background, and shown with the gray background color in the diagram.

80f7c594ae844b84.png

If you tap the Back button, the DetailActivity is popped off the back stack and the DetailActivity instance is destroyed and finished.

80f532af817191a4.png

Then the next item on top of the back stack (the MainActivity) is brought to the foreground.

85004712d2fbcdc1.png

In the same way that the back stack can keep track of activities that have been opened by the user, the back stack can also track the fragment destinations the user has visited with the help of the Jetpack Navigation component.

fe417ac5cbca4ce7.png

The Navigation library allows you to pop a fragment destination off the back stack each time the user hits the Back button. This default behavior comes for free, without you needing to implement any of it. You would only need to write code if you need custom back stack behavior, which you will be doing for the Cupcake app.

Default behavior of Cupcake app

Let's look at how the back stack works in the Cupcake app. There's only one activity in the app, but there are multiple fragment destinations that the user navigates through. Hence it's desired for the Back button to return to a previous fragment destination each time it is tapped.

When you first open the app, the StartFragment destination is shown. That destination gets pushed on top of the stack.

cf0e80b4907d80dd.png

After you select a quantity of cupcakes to order, you navigate to the FlavorFragment, which gets pushed onto the back stack.

39081dcc3e537e1e.png

When you select a flavor and tap Next, you navigate to the PickupFragment, which gets pushed onto the back stack.

37dca487200f8f73.png

And finally, once you select a pickup date and tap Next, you navigate to the SummaryFragment, which gets added to the top of the back stack.

d67689affdfae0dd.png

From the SummaryFragment, say that you tap the Back or Up button. The SummaryFragment gets popped off the stack and destroyed.

215b93fd65754017.png

The PickupFragment is now on top of the back stack and gets shown to the user.

37dca487200f8f73.png

Tap Back or Up button again. The PickupFragment is popped off the stack, and then the FlavorFragment is shown.

Tap Back or Up button again. The FlavorFragment is popped off the stack, and then the StartFragment is shown.

As you navigate backwards to earlier steps in the order flow, only one destination is popped off at a time. But in the next task, you will be adding a cancel order feature to the app. This may require you to pop off multiple destinations in the back stack at once in order to return the user to the StartFragment to start a new order.

e3dae0f492450207.png

Modify the back stack in the Cupcake app

Modify the FlavorFragment, PickupFragment, and SummaryFragment classes and layout files in order to offer a Cancel order button to the user.

Add navigation action

First add navigation actions to the navigation graph in your app, so that it's possible for the user to navigate back to the StartFragment from subsequent destinations.

  1. Open the Navigation Editor by going to the res > navigation > nav_graph.xml file and selecting the Design view.
  2. Currently, there is an action from the startFragment to flavorFragment, an action from the flavorFragment to the pickupFragment, and an action from the pickupFragment to the summaryFragment.
  3. Click and drag to create a new navigation action from summaryFragment to startFragment. You can see these instructions if you want a refresher on how to connect destinations in the navigation graph.
  4. From the pickupFragment, click and drag to create a new action to startFragment.
  5. From the flavorFragment, click and drag to create a new action to startFragment.
  6. When you're done, the navigation graph should look like the following.

dcbd27a08d24cfa0.png

With these changes, a user could traverse from one of the later fragments in the order flow back to the beginning of the order flow. Now you need code that actually navigates with those actions. The appropriate place is when the Cancel button is tapped.

Add Cancel button to layout

First add the Cancel button to the layout files for all fragments except the StartFragment. There is no need to cancel an order if you're already on the first screen of the order flow.

  1. Open the fragment_flavor.xml layout file.
  2. Use the Split view to edit the XML directly and view the preview side-by-side.
  3. Add the Cancel button in between the subtotal text view and the Next button. Assign it a resource ID @+id/cancel_button with text to display as @string/cancel.

The button should be positioned horizontally beside the Next button so that it appears as a row of buttons. For a vertical constraint, constrain the top of the Cancel button to the top of the Next button. For horizontal constraints, constrain the start of the Cancel button to the parent container, and constrain its end to the start of the Next button.

Also give the Cancel button a height of wrap_content and a width of 0dp, so it can equally split the width of the screen with the other button. Note that the button won't be visible in the Preview pane until the next step.

...

<TextView
    android:id="@+id/subtotal" ... />

<Button
    android:id="@+id/cancel_button"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:text="@string/cancel"
    app:layout_constraintEnd_toStartOf="@id/next_button"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="@id/next_button" />

<Button
    android:id="@+id/next_button" ... />

...
  1. In fragment_flavor.xml, you'll also need to change the start constraint of the Next button from app:layout_constraintStart_toStartOf="parent" to app:layout_constraintStart_toEndOf="@id/cancel_button". Also add an end margin on the Cancel button so there is some whitespace between the two buttons. Now the Cancel button should appear in the Preview pane in Android Studio.
...

<Button
    android:id="@+id/cancel_button"
    android:layout_marginEnd="@dimen/side_margin" ... />

<Button
    android:id="@+id/next_button"
    app:layout_constraintStart_toEndOf="@id/cancel_button"... />

...
  1. In terms of visual style, apply the Material Outlined Button style (with attribute style="?attr/materialButtonOutlinedStyle") so that the Cancel button doesn't appear as prominently compared to the Next button, which is the primary action that you want the user to focus on.
<Button
    android:id="@+id/cancel_button"
    style="?attr/materialButtonOutlinedStyle" ... />

The button and positioning look great now!

1fb41763cc255c05.png

  1. In the same way, add a Cancel button to the fragment_pickup.xml layout file.
...

<TextView
    android:id="@+id/subtotal" ... />

<Button
    android:id="@+id/cancel_button"
    style="?attr/materialButtonOutlinedStyle"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_marginEnd="@dimen/side_margin"
    android:text="@string/cancel"
    app:layout_constraintEnd_toStartOf="@id/next_button"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="@id/next_button" />

<Button
    android:id="@+id/next_button" ... />

... 
  1. Update the start constraint on the Next button as well. Then the Cancel button will appear in the preview.
<Button
    android:id="@+id/next_button"
    app:layout_constraintStart_toEndOf="@id/cancel_button" ... />
  1. Apply a similar change to the fragment_summary.xml file, though the layout for this fragment is slightly different. You will add the Cancel button below the Send button in the parent vertical LinearLayout with some margin in between.

741c0f034397795c.png

...

    <Button
        android:id="@+id/send_button" ... />

    <Button
        android:id="@+id/cancel_button"
        style="?attr/materialButtonOutlinedStyle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/margin_between_elements"
        android:text="@string/cancel" />

</LinearLayout>
  1. Run and test the app. You should now see the Cancel button appear in the layouts for the FlavorFragment, PickupFragment, and SummaryFragment. However, tapping on the button doesn't do anything yet. Set up the click listeners for these buttons in the next step.

Add Cancel button click listener

Within each fragment class (except StartFragment) add a helper method that handles when the Cancel button is clicked.

  1. Add this cancelOrder() method to FlavorFragment. When presented with the flavor options, if the user decides to cancel their order, then clear out the view model by calling sharedViewModel.resetOrder(). Then navigate back to the StartFragment using the navigation action with ID R.id.action_flavorFragment_to_startFragment.
fun cancelOrder() {
    sharedViewModel.resetOrder()
    findNavController().navigate(R.id.action_flavorFragment_to_startFragment)
}

If you see an error related to the action resource ID, you may need to go back to the nav_graph.xml file to verify that your navigation actions are also called the same name (action_flavorFragment_to_startFragment).

  1. Use listener binding to set up the click listener on the Cancel button in the fragment_flavor.xml layout. Clicking on this button will invoke the cancelOrder() method you just created in the FragmentFlavor class.
<Button
    android:id="@+id/cancel_button"
    android:onClick="@{() -> flavorFragment.cancelOrder()}" ... />
  1. Repeat the same process for the PickupFragment. Add a cancelOrder() method to the fragment class, which resets the order and navigates from the PickupFragment to the StartFragment.
fun cancelOrder() {
    sharedViewModel.resetOrder()
    findNavController().navigate(R.id.action_pickupFragment_to_startFragment)
}
  1. In fragment_pickup.xml, set the click listener on the Cancel button to call the cancelOrder() method when clicked.
<Button
    android:id="@+id/cancel_button"
    android:onClick="@{() -> pickupFragment.cancelOrder()}" ... />
  1. Add similar code for the Cancel button in the SummaryFragment, bringing the user back to the StartFragment. You may need to import androidx.navigation.fragment.findNavController if it's not automatically imported for you.
fun cancelOrder() {
    sharedViewModel.resetOrder()
    findNavController().navigate(R.id.action_summaryFragment_to_startFragment)
}
  1. In fragment_summary.xml, call the SummaryFragment's cancelOrder() method when the Cancel button is clicked.
<Button
    android:id="@+id/cancel_button"
    android:onClick="@{() -> summaryFragment.cancelOrder()}" ... />
  1. Run and test the app to verify the logic that you just added to each fragment. As you create a cupcake order, tapping the Cancel button on the FlavorFragment, PickupFragment, or SummaryFragment returns you back to the StartFragment. As you proceed to create a new order, you should notice that the information from your previous order has been cleared out.

This looks like it works, but there is actually a bug with navigating backwards, once you return back to the StartFragment. Follow the next few steps to reproduce the bug.

  1. Go through the order flow for creating a new cupcake order until you reach the summary screen. For example, you could order 12 cupcakes, chocolate flavor, and choose a future date for pickup.
  2. Then tap Cancel. You should return back to the StartFragment.
  3. This looks correct, but if you tap the system Back button, then you end up back on the order summary screen with an order summary for 0 cupcakes and no flavor. This is incorrect and should not be shown to the user.

1a9024cd58a0e643.png

The user likely does not want to go back through the order flow. Plus, all the order data in the view model has been cleared out, so this information is not useful. Instead, tapping the Back button from the StartFragment should leave the Cupcake app.

Let's look at what the back stack currently looks like, and how to fix the bug. When you create an order up through the order summary screen, each destination gets pushed onto the back stack.

fc88100cdf1bdd1.png

From the SummaryFragment, you cancelled the order. When you navigated via the action from SummaryFragment to StartFragment, Android added another instance of StartFragment as a new destination on the back stack.

5616cb0028b63602.png

That is why when you tapped the Back button from the StartFragment, the app ended up showing the SummaryFragment again (with blank order information).

To fix this navigation bug, learn about how the Navigation component allows you to pop off additional destinations from the back stack when navigating using an action.

Pop additional destinations off the back stack

By including an app:popUpTo attribute on the navigation action in the navigation graph, more than one destination can be popped off the back stack up until that specified destination is reached. If you specify app:popUpTo="@id/startFragment", then destinations in the back stack will get popped off until you reach StartFragment, which will remain on the stack.

When you add this change to your code and run the app, you'll find that when you cancel an order, you return to the StartFragment. But this time, when you tap the Back button from the StartFragment, you see StartFragment again (instead of exiting the app). This is also not the desired behavior. As mentioned earlier, since you are navigating to StartFragment, Android actually adds StartFragment as a new destination on the back stack, so now you have 2 instances of StartFragment on the back stack. Hence, you need to tap the Back button twice to exit the app.

dd0fedc6e231e595.png

To fix this new bug, request that all destinations are popped off the back stack up to and including the StartFragment. Do this by specifying app:popUpTo="@id/startFragment"

and app:popUpToInclusive="true" on the appropriate navigation actions. That way, you will only have the one new instance of StartFragment in the back stack. Then tapping the Back button once from the StartFragment will exit the app. Let's make this change now.

cf0e80b4907d80dd.png

Modify the navigation actions

  1. Go to the Navigation Editor by opening up the res > navigation > nav_graph.xml file.
  2. Select the action that goes from the summaryFragment to the startFragment, so it is highlighted in blue.
  3. Expand the Attributes on the right (if it's not already open). Look for Pop Behavior in the list of attributes you could modify.

8c87589f9cc4d176.png

  1. From the dropdown options, set popUpTo to be startFragment. This means all the destinations in the back stack will be popped off (starting from the top of the stack and moving downwards), up to the startFragment.

a9a17493ed6bc27f.png

  1. Then click on the checkbox for popUpToInclusive until it shows a checkmark and label true. This indicates that you want to pop off destinations up to and including the instance of startFragment that's already in the back stack. Then you won't have two instances of startFragment in the back stack.

4a403838a62ff487.png

  1. Repeat these changes for the action connecting pickupFragment to startFragment.

4a403838a62ff487.png

  1. Repeat for the action connecting flavorFragment to startFragment.
  2. When you're done, confirm you've made the correct changes to your app by looking at the Code view of the navigation graph file.
<navigation 
    android:id="@+id/nav_graph" ...>
    <fragment
        android:id="@+id/startFragment" ...>
        ...
    </fragment>
    <fragment
        android:id="@+id/flavorFragment" ...>
        ...
        <action
            android:id="@+id/action_flavorFragment_to_startFragment"
            app:destination="@id/startFragment"
            app:popUpTo="@id/startFragment"
            app:popUpToInclusive="true" />
    </fragment>
    <fragment
        android:id="@+id/pickupFragment" ...>
        ...
        <action
            android:id="@+id/action_pickupFragment_to_startFragment"
            app:destination="@id/startFragment"
            app:popUpTo="@id/startFragment"
            app:popUpToInclusive="true" />
    </fragment>
    <fragment
        android:id="@+id/summaryFragment" ...>
        <action
            android:id="@+id/action_summaryFragment_to_startFragment"
            app:destination="@id/startFragment"
            app:popUpTo="@id/startFragment"
            app:popUpToInclusive="true" />
    </fragment>
</navigation>

Notice that for each of the 3 actions (action_flavorFragment_to_startFragment, action_pickupFragment_to_startFragment, and action_summaryFragment_to_startFragment), there should be newly added attributes app:popUpTo="@id/startFragment" and app:popUpToInclusive="true".

  1. Now run the app. Go through the order flow and tap Cancel. When you return back to the StartFragment, tap the Back button (only once!) and you should leave the app.

As a summary of what is happening, when you canceled the order and navigated back to the first screen of the app, all the fragment destinations within the back stack got popped off the stack, including the first instance of StartFragment. Upon completing the navigation action, StartFragment got added as a new destination on the back stack. Tapping Back from there pops StartFragment off the stack, leaving no more fragments in the back stack. Hence Android finishes the activity and the user leaves the app.

Here is what the app should look like: 2e0599d9b55401f1.png

5. Send the order

The app looks fantastic so far! There is one part missing though. When you tap on the send order button on the SummaryFragment, a Toast message still pops up.

90ed727c7b812fd6.png

It would be a more useful experience if the order could be sent out from the app. Take advantage of what you learned in earlier codelabs about using an implicit intent to share information from your app to another app. That way, the user can share the cupcake order information with an email app on the device, allowing the order to be emailed to the cupcake shop.

170d76b64ce78f56.png

To implement this feature, take a look at how the email subject and email body are structured in the above screenshot.

You'll be using these strings that are already in your strings.xml file.

<string name="new_cupcake_order">New Cupcake Order</string>
<string name="order_details">Quantity: %1$s cupcakes \n Flavor: %2$s \nPickup date: %3$s \n Total: %4$s \n\n Thank you!</string>

order_details is a string resource with 4 different format arguments in it, which are placeholders for the actual quantity of cupcakes, desired flavor, desired pickup date, and total price. The arguments are numbered from 1 to 4 with the syntax %1 to %4. The type of argument is also specified ($s means a string is expected here).

In Kotlin code, you will be able to call getString() on R.string.order_details followed by the 4 arguments (order matters!). As an example, calling getString(R.string.order_details, "12", "Chocolate", "Sat Dec 12", "$24.00") creates the following string, which is exactly the email body you want.

Quantity: 12 cupcakes
Flavor: Chocolate
Pickup date: Sat Dec 12
Total: $24.00

Thank you!
  1. In SummaryFragment.kt modify the sendOrder() method. Remove the existing Toast message.
fun sendOrder() {
  
}
  1. Within the sendOrder() method, construct the order summary text. Create the formatted order_details string by getting the order quantity, flavor, date, and price from the shared view model.
val orderSummary = getString(
    R.string.order_details,
    sharedViewModel.quantity.value.toString(),
    sharedViewModel.flavor.value.toString(),
    sharedViewModel.date.value.toString(),
    sharedViewModel.price.value.toString()
)
  1. Still within the sendOrder() method, create an implicit intent for sharing the order to another app. See the documentation for how to create an email intent. Specify Intent.ACTION_SEND for the intent action, set type to "text/plain" and include intent extras for the email subject (Intent.EXTRA_SUBJECT) and email body (Intent.EXTRA_TEXT). Import android.content.Intent if needed.
val intent = Intent(Intent.ACTION_SEND)
    .setType("text/plain")
    .putExtra(Intent.EXTRA_SUBJECT, getString(R.string.new_cupcake_order))
    .putExtra(Intent.EXTRA_TEXT, orderSummary)

As a bonus tip, if you adapt this app to your own use case, you could pre-populate the recipient of the email to be the email address of the cupcake shop. In the intent, you would specify the email recipient with intent extra Intent.EXTRA_EMAIL.

  1. Since this is an implicit intent, you don't need to know ahead of time which specific component or app will handle this intent. The user will decide which app they want to use to fulfill the intent. However, before launching an activity with this intent, check to see if there's an app that could even handle it. This check will prevent the Cupcake app from crashing if there's no app to handle the intent, making your code safer.
if (activity?.packageManager?.resolveActivity(intent, 0) != null) {
    startActivity(intent)
}

Perform this check by accessing the PackageManager, which has information about what app packages are installed on the device. The PackageManager can be accessed via the fragment's activity, as long as the activity and packageManager are not null. Call the PackageManager's resolveActivity() method with the intent you created. If the result is not null, then it is safe to call startActivity() with your intent.

  1. Run your app to test your code. Create a cupcake order and tap Send Order to Another App. When the share dialog pops up, you can select the Gmail app, but feel free to choose a different app if you prefer. If you choose the Gmail app, you may need to set up an account on the device if you haven't done so already (for example, if you're using the emulator). If you aren't seeing your latest cupcake order appear in the email body, you may need to discard the current email draft first.

170d76b64ce78f56.png

In testing different scenarios, you may notice a bug if you only have 1 cupcake. The order summary says 1 cupcakes, but in English, this is grammatically incorrect.

ef046a100381bb07.png

Instead, it should say 1 cupcake (no plural). If you want to choose whether the word cupcake or cupcakes is used based on the quantity value, then you can use something called quantity strings in Android. By declaring a plurals resource, you can specify different string resources to use based on what the quantity is, for example in the singular or plural case.

  1. Add a cupcakes plurals resource in your strings.xml file.
<plurals name="cupcakes">
    <item quantity="one">%d cupcake</item>
    <item quantity="other">%d cupcakes</item>
</plurals>

In the singular case (quantity="one"), the singular string will be used. In all other cases (quantity="other"), the plural string will be used. Note that instead of %s which expects a string argument, %d expects an integer argument, which you will pass in when you format the string.

In your Kotlin code, calling:

getQuantityString(R.plurals.cupcakes, 1, 1) returns the string 1 cupcake

getQuantityString(R.plurals.cupcakes, 6, 6) returns the string 6 cupcakes

getQuantityString(R.plurals.cupcakes, 0, 0) returns the string 0 cupcakes

  1. Before going to your Kotlin code, update the order_details string resource in strings.xml so that the plural version of cupcakes is no longer hardcoded into it.
<string name="order_details">Quantity: %1$s \n Flavor: %2$s \nPickup date: %3$s \n
        Total: %4$s \n\n Thank you!</string>
  1. In the SummaryFragment class, update your sendOrder() method to use the new quantity string. It would be easiest to first figure out the quantity from the view model and store that in a variable. Since quantity in the view model is of type LiveData<Int>, it's possible that sharedViewModel.quantity.value is null. If it is null, then use 0 as the default value for numberOfCupcakes.

Add this as the first line of code in your sendOrder() method.

val numberOfCupcakes = sharedViewModel.quantity.value ?: 0

The elvis operator (?:) means that if the expression on the left is not null, then use it. Otherwise if the expression on the left is null, then use the expression to the right of the elvis operator (which is 0 in this case).

  1. Then format the order_details string as you did before. Instead of passing in numberOfCupcakes as the quantity argument directly, create the formatted cupcakes string with resources.getQuantityString(R.plurals.cupcakes, numberOfCupcakes, numberOfCupcakes).

The full sendOrder() method should look like the following:

fun sendOrder() {
    val numberOfCupcakes = sharedViewModel.quantity.value ?: 0
    val orderSummary = getString(
        R.string.order_details,
        resources.getQuantityString(R.plurals.cupcakes, numberOfCupcakes, numberOfCupcakes),
        sharedViewModel.flavor.value.toString(),
        sharedViewModel.date.value.toString(),
        sharedViewModel.price.value.toString()
    )

    val intent = Intent(Intent.ACTION_SEND)
        .setType("text/plain")
        .putExtra(Intent.EXTRA_SUBJECT, getString(R.string.new_cupcake_order))
        .putExtra(Intent.EXTRA_TEXT, orderSummary)
    
    if (activity?.packageManager?.resolveActivity(intent, 0) != null) {
        startActivity(intent)
    }
}
  1. Run and test your code. Check that the order summary in the email body shows 1 cupcake vs. 6 cupcakes or 12 cupcakes.

With that, you have completed all the functionality of the Cupcake app! Congratulations!! This was certainly a challenging app, and you've made tremendous progress in your journey to becoming an Android developer! You were able to successfully combine all the concepts you've learned thus far, while picking up some new problem solving tips along the way.

Final steps

Now take some time to clean up your code, which is a good coding practice you've learned from previous codelabs.

  • Optimize imports
  • Reformat the files
  • Remove unused or commented out code
  • Add comments in the code where necessary

To make your app more accessible, test your app with Talkback enabled to ensure a smooth user experience. The spoken feedback should help convey the purpose of each element on the screen, where appropriate. Also ensure that all elements of the app can be navigated to using the swipe gestures.

Double check that the use cases you implemented all work as expected in your final app. Examples:

  • Data should be preserved on device rotation (thanks to view model).
  • If you tap the Up or Back button, the order information should still appear correctly on FlavorFragment and PickupFragment.
  • Sending the order to another app should share the correct order details.
  • Canceling an order should clear out all information in the order.

If you find any bugs, go ahead and fix them.

Nice job on double checking your work!

6. Solution code

The solution code for this codelab is in the project shown below.

To get the code for this codelab and open it in Android Studio, do the following.

Get the code

  1. Click on the provided URL. This opens the GitHub page for the project in a browser.
  2. On the GitHub page for the project, click the Code button, which brings up a dialog.

5b0a76c50478a73f.png

  1. In the dialog, click the Download ZIP button to save the project to your computer. Wait for the download to complete.
  2. Locate the file on your computer (likely in the Downloads folder).
  3. Double-click the ZIP file to unpack it. This creates a new folder that contains the project files.

Open the project in Android Studio

  1. Start Android Studio.
  2. In the Welcome to Android Studio window, click Open an existing Android Studio project.

36cc44fcf0f89a1d.png

Note: If Android Studio is already open, instead, select the File > New > Import Project menu option.

21f3eec988dcfbe9.png

  1. In the Import Project dialog, navigate to where the unzipped project folder is located (likely in your Downloads folder).
  2. Double-click on that project folder.
  3. Wait for Android Studio to open the project.
  4. Click the Run button 11c34fc5e516fb1c.png to build and run the app. Make sure it builds as expected.
  5. Browse the project files in the Project tool window to see how the app is set-up.

7. Summary

  • Android keeps a back stack of all the destinations you've visited, with each new destination being pushed onto the stack.
  • By tapping the Up or Back button, you can pop destinations off the back stack.
  • Using the Jetpack Navigation component helps you push and pop fragment destinations off the back stack, so that the default Back button behavior comes for free.
  • Specify the app:popUpTo attribute on an action in the navigation graph, in order to pop destinations off the back stack until the specified one in the attribute value.
  • Specify app:popUpToInclusive="true" on an action when the destination specified in app:popUpTo should also be popped off the back stack.
  • You can create an implicit intent to share content to an email app, using Intent.ACTION_SEND and populating intent extras such as Intent.EXTRA_EMAIL, Intent.EXTRA_SUBJECT, and Intent.EXTRA_TEXT to name a few.
  • Use a plurals resource if you want to use different string resources based on quantity, such as the singular or plural case.

8. Learn more

9. Practice on your own

Extend the Cupcake app with your own variations on the cupcake order flow. Examples:

  • Offer a special flavor that has some special conditions around it, such as not being available for same day pickup.
  • Ask the user for their name for the cupcake order.
  • Allow the user to select multiple cupcake flavors for their order if the quantity is more than 1 cupcake.

What areas of your app would you need to update to accommodate this new functionality?

Check your work:

Your finished app should run without errors.

10. Challenge Task

Use what you've learned from building the Cupcake app to build an app for your own use case. It can be an app for ordering pizza, sandwiches, or anything else you can think of! It's recommended that you sketch out the different destinations of your app before starting to implement it.

To get inspiration from other design ideas, you can also check out the Shrine app which is a Material study that shows how you can adopt Material theming and components for your own brand. The Shrine app is much more complex than the Cupcake app you've built, so instead of aiming to build a very challenging app upfront, think about small features you can tackle first. Build your confidence over time with incremental and gradual wins.

Once you've finished creating your own app, share what you've built on social media. Use the hashtag #LearningKotlin so we can see it!