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
- Click on the provided URL. This opens the GitHub page for the project in a browser.
- On the GitHub page for the project, click the Code button, which brings up a dialog.
- In the dialog, click the Download ZIP button to save the project to your computer. Wait for the download to complete.
- Locate the file on your computer (likely in the Downloads folder).
- Double-click the ZIP file to unpack it. This creates a new folder that contains the project files.
Open the project in Android Studio
- Start Android Studio.
- In the Welcome to Android Studio window, click Open an existing Android Studio project.
Note: If Android Studio is already open, instead, select the File > New > Import Project menu option.
- In the Import Project dialog, navigate to where the unzipped project folder is located (likely in your Downloads folder).
- Double-click on that project folder.
- Wait for Android Studio to open the project.
- Click the Run button
to build and run the app. Make sure it builds as expected.
- 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.
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.
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.
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.
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.
- In the
MainActivity
, you should already have code to set up the app bar (also known as action bar) with the nav controller. MakenavController
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)
}
}
- Within the same class, add code to override the
onSupportNavigateUp()
function. This code will ask thenavController
to handle navigating up in the app. Otherwise, fall back to back to the superclass implementation (inAppCompatActivity
) of handling the Up button.
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp() || super.onSupportNavigateUp()
}
- Run the app. The Up button should now work from the
FlavorFragment
,PickupFragment
, andSummaryFragment
. 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.
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.
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.
If you tap the Back button, the DetailActivity
is popped off the back stack and the DetailActivity
instance is destroyed and finished.
Then the next item on top of the back stack (the MainActivity
) is brought to the foreground.
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.
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.
After you select a quantity of cupcakes to order, you navigate to the FlavorFragment
, which gets pushed onto the back stack.
When you select a flavor and tap Next, you navigate to the PickupFragment
, which gets pushed onto the back stack.
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.
From the SummaryFragment
, say that you tap the Back or Up button. The SummaryFragment
gets popped off the stack and destroyed.
The PickupFragment
is now on top of the back stack and gets shown to the user.
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.
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.
- Open the Navigation Editor by going to the res > navigation > nav_graph.xml file and selecting the Design view.
- Currently, there is an action from the
startFragment
toflavorFragment
, an action from theflavorFragment
to thepickupFragment
, and an action from thepickupFragment
to thesummaryFragment
. - Click and drag to create a new navigation action from
summaryFragment
tostartFragment
. You can see these instructions if you want a refresher on how to connect destinations in the navigation graph. - From the
pickupFragment
, click and drag to create a new action tostartFragment
. - From the
flavorFragment
, click and drag to create a new action tostartFragment
. - When you're done, the navigation graph should look like the following.
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.
- Open the
fragment_flavor.xml
layout file. - Use the Split view to edit the XML directly and view the preview side-by-side.
- 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" ... />
...
- In
fragment_flavor.xml
, you'll also need to change the start constraint of the Next button fromapp:layout_constraintStart_toStartOf="parent
" toapp: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"... />
...
- 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!
- 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" ... />
...
- 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" ... />
- 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 verticalLinearLayout
with some margin in between.
...
<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>
- Run and test the app. You should now see the Cancel button appear in the layouts for the
FlavorFragment
,PickupFragment
, andSummaryFragment
. 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.
- Add this
cancelOrder()
method toFlavorFragment
. When presented with the flavor options, if the user decides to cancel their order, then clear out the view model by callingsharedViewModel.resetOrder().
Then navigate back to theStartFragment
using the navigation action with IDR.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
).
- 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 thecancelOrder()
method you just created in theFragmentFlavor
class.
<Button
android:id="@+id/cancel_button"
android:onClick="@{() -> flavorFragment.cancelOrder()}" ... />
- Repeat the same process for the
PickupFragment
. Add acancelOrder()
method to the fragment class, which resets the order and navigates from thePickupFragment
to theStartFragment
.
fun cancelOrder() {
sharedViewModel.resetOrder()
findNavController().navigate(R.id.action_pickupFragment_to_startFragment)
}
- In
fragment_pickup.xml
, set the click listener on the Cancel button to call thecancelOrder()
method when clicked.
<Button
android:id="@+id/cancel_button"
android:onClick="@{() -> pickupFragment.cancelOrder()}" ... />
- Add similar code for the Cancel button in the
SummaryFragment
, bringing the user back to theStartFragment
. You may need to importandroidx.navigation.fragment.findNavController
if it's not automatically imported for you.
fun cancelOrder() {
sharedViewModel.resetOrder()
findNavController().navigate(R.id.action_summaryFragment_to_startFragment)
}
- In
fragment_summary.xml
, call theSummaryFragment
'scancelOrder()
method when the Cancel button is clicked.
<Button
android:id="@+id/cancel_button"
android:onClick="@{() -> summaryFragment.cancelOrder()}" ... />
- 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
, orSummaryFragment
returns you back to theStartFragment
. 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.
- 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.
- Then tap Cancel. You should return back to the
StartFragment
. - 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.
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.
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.
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
Navigation action: popUpTo attribute
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.
Navigation action: popUpToInclusive attribute
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.
Modify the navigation actions
- Go to the Navigation Editor by opening up the res > navigation > nav_graph.xml file.
- Select the action that goes from the
summaryFragment
to thestartFragment
, so it is highlighted in blue. - Expand the Attributes on the right (if it's not already open). Look for Pop Behavior in the list of attributes you could modify.
- 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 thestartFragment
.
- 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 ofstartFragment
in the back stack.
- Repeat these changes for the action connecting
pickupFragment
tostartFragment
.
- Repeat for the action connecting
flavorFragment
tostartFragment
. - 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"
.
- 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:
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.
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.
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!
- In
SummaryFragment.kt
modify thesendOrder()
method. Remove the existingToast
message.
fun sendOrder() {
}
- Within the
sendOrder()
method, construct the order summary text. Create the formattedorder_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()
)
- 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. SpecifyIntent.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
). Importandroid.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
.
- 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.
- 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.
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.
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.
- Add a
cupcakes
plurals resource in yourstrings.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
- Before going to your Kotlin code, update the
order_details
string resource instrings.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>
- In the
SummaryFragment
class, update yoursendOrder()
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. Sincequantity
in the view model is of typeLiveData<Int>
, it's possible thatsharedViewModel.quantity.value
is null. If it is null, then use0
as the default value fornumberOfCupcakes
.
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).
- Then format the
order_details
string as you did before. Instead of passing innumberOfCupcakes
as the quantity argument directly, create the formatted cupcakes string withresources.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)
}
}
- 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
andPickupFragment
. - 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
- Click on the provided URL. This opens the GitHub page for the project in a browser.
- On the GitHub page for the project, click the Code button, which brings up a dialog.
- In the dialog, click the Download ZIP button to save the project to your computer. Wait for the download to complete.
- Locate the file on your computer (likely in the Downloads folder).
- Double-click the ZIP file to unpack it. This creates a new folder that contains the project files.
Open the project in Android Studio
- Start Android Studio.
- In the Welcome to Android Studio window, click Open an existing Android Studio project.
Note: If Android Studio is already open, instead, select the File > New > Import Project menu option.
- In the Import Project dialog, navigate to where the unzipped project folder is located (likely in your Downloads folder).
- Double-click on that project folder.
- Wait for Android Studio to open the project.
- Click the Run button
to build and run the app. Make sure it builds as expected.
- 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 inapp: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 asIntent.EXTRA_EMAIL
,Intent.EXTRA_SUBJECT
, andIntent.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!