1. Before you begin
This codelab introduces a new app called Lunch Tray that you'll build on your own. This codelab walks you through the steps to complete the Lunch Tray app project, including project setup and testing within Android Studio.
This codelab is different from the others in this course. Unlike previous codelabs, the purpose of this codelab is not to provide a step-by-step tutorial on how to build an app. Instead, this codelab is meant to set up a project that you will complete independently, providing you with instructions on how to complete an app and check your work on your own.
Instead of solution code, we provide a test suite as part of the app you'll download. You'll run these tests in Android Studio (we'll show you how to do this later in this codelab) and see if your code passes. This may take a few tries—even professional developers rarely have all their tests pass on the first try! After your code passes all the tests, you can consider this project as complete.
We understand that you might just want the solution to check against. We deliberately do not provide the solution code because we want you to practice what it's like to be a professional developer. This might require you to use different skills that you don't yet have a lot of practice with, such as:
- Googling terms, error messages, and bits of code in the app that you don't recognize;
- Testing code, reading errors, then making changes to the code and testing it again;
- Going back to previous content in Android Basics to refresh what you've learned;
- Comparing code that you know works (i.e. code that is given in the project, or past solution code from other apps in Unit 3) with the code you're writing.
This might seem daunting at first, but we are 100 percent confident that if you were able to complete Unit 3, you are ready for this project. Take your time, and don't give up. You can do this.
Prerequisites
- This project is for users who have completed Unit 3 of the Android Basics in Kotlin course.
What you'll build
- You'll take a food ordering app called Lunch Tray, implement a ViewModel with data binding, and add navigation between fragments.
What you'll need
- A computer with Android Studio installed.
2. Finished app overview
Welcome to Project: Lunch Tray!
As you're probably aware, navigation is a fundamental part of Android development. Whether you're using an app to browse recipes, find directions to your favorite restaurant, or most importantly, order food, chances are that you're navigating multiple screens of content. In this project, you'll leverage the skills you learned in Unit 3 to build out a lunch ordering app called Lunch Tray, implementing a view model, data binding, and navigation between screens.
Below are the final app screenshots. When first launching the Lunch Tray app, the user is greeted with a screen with a single button that says "Start Order".
After clicking Start Order, the user can then select an entree from the available choices. The user can change their selection, which updates the Subtotal shown at the bottom.
The next screen allows the user to add a side dish.
The screen after that allows the user to select an accompaniment for their order.
Finally, the user is shown a summary of the cost of their order, broken down into subtotal, sales tax, and total cost. They can also submit or cancel the order.
Both options return the user to the first screen. If the user submitted the order, a toast should appear at the bottom of the screen letting them know that the order has been submitted.
3. Get started
Download the project code
Note that the folder name is android-basics-kotlin-lunch-tray-app
. Select this folder when you open the project in Android Studio.
- Navigate to the provided GitHub repository page for the project.
- Verify that the branch name matches the branch name specified in the codelab. For example, in the following screenshot the branch name is main.
- On the GitHub page for the project, click the Code button, which brings up a popup.
- In the popup, 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.
Note: If Android Studio is already open, instead, select the File > Open menu option.
- In the file browser, 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.
Before you start implementing the ViewModel
and navigation, take a moment to make sure the project builds successfully and to familiarize yourself with the project. When you run the app for the first time, you'll see an empty screen. The MainActivity
isn't presenting any fragments, as you haven't set up the navigation graph yet.
The project structure should be similar to other projects you've worked with. Separate packages are provided for the data, model, and ui, as well as separate directories for resources.
All the lunch options the user can order (entrees, sides, and accompaniments) are represented by the MenuItem
class in the model package. MenuItem
objects have a name, description, price, and type.
data class MenuItem(
val name: String,
val description: String,
val price: Double,
val type: Int
) {
fun getFormattedPrice(): String = NumberFormat.getCurrencyInstance().format(price)
}
The type is represented by an integer that comes from the ItemType
object in the constants package.
object ItemType {
val ENTREE = 1
val SIDE_DISH = 2
val ACCOMPANIMENT = 3
}
Individual MenuItem
objects can be found in DataSource.kt
in the data package.
object DataSource {
val menuItems = mapOf(
"cauliflower" to
MenuItem(
name = "Cauliflower",
description = "Whole cauliflower, brined, roasted, and deep fried",
price = 7.00,
type = ItemType.ENTREE
),
...
}
This object simply contains a map consisting of a key and a corresponding MenuItem
. You'll access the DataSource
from ObjectViewModel
, which you'll implement first.
Define the ViewModel
As seen in the screenshots on the previous page, the app asks for three things from the user: an entree, a side, and an accompaniment. The order summary screen then shows a subtotal and calculates sales tax based on the selected items, which are used to calculate the order total.
In the model package, open up OrderViewModel.kt
and you'll see that a few variables are already defined. The menuItems
property simply allows you to access the DataSource
from the ViewModel
.
val menuItems = DataSource.menuItems
First, there are also some variables for previousEntreePrice
, previousSidePrice
, and previousAccompanimentPrice
. Because the subtotal is updated as the user makes their choice (rather than being added up at the end), these variables are used to keep track of the user's previous selection if they change their selection before moving to the next screen. You'll use these to ensure the subtotal accounts for the difference between prices of the previous and currently selected items.
private var previousEntreePrice = 0.0
private var previousSidePrice = 0.0
private var previousAccompanimentPrice = 0.0
There are also private variables, _entree
, _side
, and _accompaniment
, for storing the currently selected choice. These are of type MutableLiveData<MenuItem?>
. Each one is accompanied by a public backing property, entree
, side
, and accompaniment
, of immutable type LiveData<MenuItem?>
. These are accessed by the fragments' layouts to show the selected item on screen. The MenuItem
contained in the LiveData
object is also nullable since it's possible for the user to not select an entree, side, and/or accompaniment.
// Entree for the order
private val _entree = MutableLiveData<MenuItem?>()
val entree: LiveData<MenuItem?> = _entree
// Side for the order
private val _side = MutableLiveData<MenuItem?>()
val side: LiveData<MenuItem?> = _side
// Accompaniment for the order.
private val _accompaniment = MutableLiveData<MenuItem?>()
val accompaniment: LiveData<MenuItem?> = _accompaniment
There are also LiveData
variables for the subtotal, total, and tax, which use number formatting so that they're displayed as currency.
// Subtotal for the order
private val _subtotal = MutableLiveData(0.0)
val subtotal: LiveData<String> = Transformations.map(_subtotal) {
NumberFormat.getCurrencyInstance().format(it)
}
// Total cost of the order
private val _total = MutableLiveData(0.0)
val total: LiveData<String> = Transformations.map(_total) {
NumberFormat.getCurrencyInstance().format(it)
}
// Tax for the order
private val _tax = MutableLiveData(0.0)
val tax: LiveData<String> = Transformations.map(_tax) {
NumberFormat.getCurrencyInstance().format(it)
}
Finally, the tax rate is a hardcoded value of 0.08 (8%).
private val taxRate = 0.08
There are six methods in OrderViewModel
that you'll need to implement.
setEntree(), setSide(), and setAccompaniment()
All of these methods should work the same way for the entree, side, and accompaniment respectively. As an example, the setEntree()
should do the following:
- If the
_entree
is notnull
(i.e. the user already selected an entree, but changed their choice), set thepreviousEntreePrice
to thecurrent _entree
's price. - If the
_subtotal
is notnull
, subtract thepreviousEntreePrice
from the subtotal. - Update the value of
_entree
to the entree passed into the function (access theMenuItem
usingmenuItems
). - Call
updateSubtotal()
, passing in the newly selected entree's price.
The logic for setSide()
and setAccompaniment()
is identical to the implementation for setEntree()
.
updateSubtotal()
updateSubtotal()
is called with an argument for the new price that should be added to the subtotal. This method should do three things:
- If
_subtotal
is notnull
, add theitemPrice
to the_subtotal
. - Otherwise, if
_subtotal
isnull
, set the_subtotal
to theitemPrice
. - After
_subtotal
has been set (or updated), callcalculateTaxAndTotal()
so that these values are updated to reflect the new subtotal.
calculateTaxAndTotal()
calculateTaxAndTotal()
should update the variables for the tax and total based on the subtotal. Implement the method as follows:
- Set the
_tax
equal to the tax rate times the subtotal. - Set the
_total
equal to the subtotal plus the tax.
resetOrder()
resetOrder()
will be called when the user submits or cancels an order. You want to make sure your app doesn't have any data left over when the user starts a new order.
Implement resetOrder()
by setting all the variables that you modified in OrderViewModel
back to their original value (either 0.0 or null).
Create data binding variables
Implement data binding in the layout files. Open up the layout files, and add data binding variables of type OrderViewModel
and/or the corresponding fragment class.
You'll need to implement all the TODO
comments to set the text and click listeners in four layout files:
fragment_entree_menu.xml
fragment_side_menu.xml
fragment_accompaniment_menu.xml
fragment_checkout.xml
Each specific task is noted in a TODO comment in the layout files, but the steps are summarized below.
- In
fragment_entree_menu.xml
, in the<data>
tag, add a binding variable for theEntreeMenuFragment
. For each of the radio buttons, you'll need to set the entree in theViewModel
when it's selected. The subtotal text view's text should be updated accordingly. You'll also need to set theonClick
attribute for thecancel_button
andnext_button
to cancel the order or navigate to the next screen respectively. - Do the same thing in
fragment_side_menu.xml
, adding a binding variable for theSideMenuFragment
, except to set the side in the view model when each radio button is selected. The subtotal text will also need to be updated, and you'll also need to set theonClick
attribute for the cancel and next buttons. - Do the same thing once more, but in
fragment_accompaniment_menu.xml
, this time with a binding variable forAccompanimentMenuFragment
, setting the accompaniment when each radio button is selected. Again, you'll also need to set attributes for the subtotal text, cancel button and next button. - In
fragment_checkout.xml
, you'll need to add a<data>
tag so that you can define binding variables. Within the<data>
tag, add two binding variables, one for theOrderViewModel
, and another for theCheckoutFragment
. In the text views, you'll need to set the names and prices of the selected entree, side dish, and accompaniment from theOrderViewModel
. You'll also need to set the subtotal, tax, and total from theOrderViewModel
. Then, set theonClickAttributes
for when the order is submitted, and when the order is canceled, using the appropriate functions fromCheckoutFragment
.
.
Initialize the data binding variables in the fragments
Initialize the data binding variables in the corresponding fragment files inside the method, onViewCreated()
.
EntreeMenuFragment
SideMenuFragment
AccompanimentMenuFragment
CheckoutFragment
Create the navigation graph
As you learned in Unit 3, a navigation graph is hosted in a FragmentContainerView
, contained in an activity. Open activity_main.xml
and replace the TODO with the following code to declare a FragmentContainerView
.
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/mobile_navigation"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
The navigation graph, mobile_navigation.xml
is found in the res.navigation package.
This is the navigation graph for the app. However, the file is currently empty. Your task is to add destinations to the navigation graph and model the following navigation between screens.
- Navigation from
StartOrderFragment
toEntreeMenuFragment
- Navigation from
EntreeMenuFragment
toSideMenuFragment
- Navigation from
SideMenuFragment
toAccompanimentMenuFragment
- Navigation from
AccompanimentMenuFragment
toCheckoutFragment
- Navigation from
CheckoutFragment
toStartOrderFragment
- Navigation from
EntreeMenuFragment
toStartOrderFragment
- Navigation from
SideMenuFragment
toStartOrderFragment
- Navigation from
AccompanimentMenuFragment
toStartOrderFragment
- The Start Destination should be
StartOrderFragment
Once you've set up the navigation graph, you'll need to perform navigation in the fragment classes. Implement the remaining TODO
comments in the fragments, as well as MainActivity.kt
.
- For the
goToNextScreen()
method inEntreeMenuFragment
,SideMenuFragment
, andAccompanimentMenuFragment
, navigate to the next screen in the app. - For the
cancelOrder()
method inEntreeMenuFragment
,SideMenuFragment
,AccompanimentMenuFragment
, andCheckoutFragment
, first callresetOrder()
on thesharedViewModel
, and then navigate to theStartOrderFragment
. - In
StartOrderFragment
, implement thesetOnClickListener()
to navigate to theEntreeMenuFragment
. - In
CheckoutFragment
, implement thesubmitOrder()
method. CallresetOrder()
on thesharedViewModel
, and then navigate to theStartOrderFragment
. - Finally, in
MainActivity.kt
, set thenavController
to thenavController
from theNavHostFragment
.
4. Test your app
The Lunch Tray project contains an "androidTest" target with several test cases: MenuContentTests
, NavigationTests
, and OrderFunctionalityTests
.
Running your tests
To run your tests, you can do one of the following:
For a single test case, open up a test case class and click the green arrow to the left of the class declaration. You can then select the Run option from the menu. This will run all of the tests in the test case.
Often you'll only want to run a single test, for example, if there's only one failing test and the other tests pass. You can run a single test just as you would the entire test case. Use the green arrow and select the Run option.
If you have multiple test cases, you can also run the entire test suite. Just like running the app, you can find this option on the Run menu.
Note that Android Studio will default to the last target that you ran (app, test targets, etc.) so if the menu still says Run > Run ‘app', you can run the test target, by selecting Run > Run.
Then choose the test target from the popup menu.
5. Optional: Give us your feedback!
We'd love to hear your opinions on this project. Take this short survey to give us feedback - your feedback will help guide future projects in this course.