Project: Lunch Tray app

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".

9e25340c8f139f7a.png

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.

11d4a4c77e7e5fc6.png

The next screen allows the user to add a side dish.

a21a5d4ad1d674f5.png

The screen after that allows the user to select an accompaniment for their order.

3785f4a1ab074cf4.png

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.

f8916d76265c0da9.png

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.

80699b621adda4d8.png

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.

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.

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.

881a368144f828a0.png

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:

  1. If the _entree's value is not null (i.e. the user already selected an entree, but changed their choice), set the previousEntreePrice to the current _entree's price.
  2. If the _subtotal's value is null, subtract the previousEntreePrice from the subtotal.
  3. Update the _entree's value to the entree passed into the function (access the MenuItem using menuItems).
  4. 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:

  1. If _subtotal is not null, add the itemPrice to the _subtotal.
  2. Otherwise, if _subtotal is null, set the _subtotal to the itemPrice.
  3. After _subtotal has been set (or updated), call calculateTaxAndTotal() 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:

  1. Set the _tax equal to the tax rate times the subtotal.
  2. 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:

  1. fragment_entree_menu.xml
  2. fragment_side_menu.xml
  3. fragment_accompaniment_menu.xml
  4. fragment_checkout.xml

Each specific task is noted in a TODO comment in the layout files, but the steps are summarized below.

  1. In fragment_entry_menu.xml, in the <data> tag, add a binding variable for the EntreeMenuFragment. For each of the radio buttons, you'll need to set the entree in the ViewModel when it's selected. The subtotal text view's text should be updated accordingly. You'll also need to set the onClick attribute for the cancel_button and next_button to cancel the order or navigate to the next screen respectively.
  2. Do the same thing in fragment_side_menu.xml, adding a binding variable for the SideMenuFragment, 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 the onClick attribute for the cancel and next buttons.
  3. Do the same thing once more, but in fragment_accompaniment_menu.xml, this time with a binding variable for AccompanimentMenuFragment, 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.
  4. 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 the OrderViewModel, and another for the CheckoutFragment. In the text views, you'll need to set the names and prices of the selected entree, side dish, and accompaniment from the OrderViewModel. You'll also need to set the subtotal, tax, and total from the OrderViewModel. Then, set the onClickAttributes for when the order is submitted, and when the order is canceled, using the appropriate functions from CheckoutFragment.

.

Initialize the data binding variables in the fragments

Initialize the data binding variables in the corresponding fragment files inside the method, onViewCreated().

  1. EntreeMenuFragment
  2. SideMenuFragment
  3. AccompanimentMenuFragment
  4. 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.

76df37040ce88193.png

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.

  1. Navigation from StartOrderFragment to EntreeMenuFragment
  2. Navigation from EntreeMenuFragment to SideMenuFragment
  3. Navigation from SideMenuFragment to AccompanimentMenuFragment
  4. Navigation from AccompanimentMenuFragment to CheckoutFragment
  5. Navigation from CheckoutFragment to StartOrderFragment
  6. Navigation from EntreeMenuFragment to StartOrderFragment
  7. Navigation from SideMenuFragment to StartOrderFragment
  8. Navigation from AccompanimentMenuFragment to StartOrderFragment
  9. 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.

  1. For the goToNextScreen() method in EntreeMenuFragment, SideMenuFragment, and AccompanimentMenuFragment, navigate to the next screen in the app.
  2. For the cancelOrder() method in EntreeMenuFragment, SideMenuFragment, AccompanimentMenuFragment, and CheckoutFragment, first call resetOrder() on the sharedViewModel, and then navigate to the StartOrderFragment.
  3. In StartOrderFragment, implement the setOnClickListener() to navigate to the EntreeMenuFragment.
  4. In CheckoutFragment, implement the submitOrder() method. Call resetOrder() on the sharedViewModel, and then navigate to the StartOrderFragment.
  5. Finally, in MainActivity.kt, set the navController to the navController from the NavHostFragment.

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.

3f8f963fd6b3e894.png

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.

90a602537504ef83.png

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.

4ab70aa8207f469.png

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.

8a3d33e6b635dac2.png

Then choose the test target from the popup menu.

9154bfbaa26d1c91.png

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.