專案:Lunch Tray 應用程式

1. 事前準備

本程式碼研究室將說明如何自行建構一款名為「Lunch Tray」的新應用程式。我們會引導您逐步完成 Lunch Tray 應用程式專案,包括在 Android Studio 中設定和測試專案。

本程式碼研究室與本課程中的其他部分不同。與先前的程式碼研究室不同,本程式碼研究室的目的並不是逐步說明如何建構應用程式,而是設定將由您獨力完成的專案,提供自行完成應用程式及檢查工作成果的相關指示。

我們改為一併在您將下載的應用程式中提供測試套件,而非程式碼解答。您將在 Android Studio 中執行這些測試 (本程式碼研究室稍後會說明操作方法),並查看程式碼是否通過測試。這可能需要多試幾次,即使是專業開發人員也很難第一次嘗試就通過所有測試!程式碼通過所有測試後,您就能將這項專案視為完成。

我們瞭解,您可能只是想獲得解答來對照檢查。我們特意不提供程式碼解答,是因為希望您能透過練習,體驗專業開發人員的作業環境。您可能會需要用到較不嫻熟的其他技能,例如:

  • 在 Google 上搜尋您在應用程式中不認得的字詞、錯誤訊息和程式碼片段。
  • 測試程式碼、解讀錯誤,然後變更程式碼並重複測試。
  • 回去閱讀先前 Android 基本概念中的內容,溫故知新。
  • 將您知道可順利執行的程式碼 (例如專案內提供的程式碼,或是單元 3 中其他應用程式先前的解決方案程式碼) 與您編寫的程式碼進行比對。

乍看之下可能很困難,但我們百分之百相信如果您能夠完成單元 3,就已經對這項專案做好準備了。請按照自己的步調進行,不要放棄,我們對您有信心。

必要條件

  • 此專案適用於已完成 Kotlin 課程中 Android 基本概念單元 3 的使用者。

建構項目

  • 您會建構一個名為 Lunch Tray 的訂餐應用程式、透過資料繫結實作 ViewModel,並在片段之間加入導覽功能。

軟硬體需求

  • 已安裝 Android Studio 的電腦。

2. 已完成應用程式總覽

歡迎來到專案:Lunch Tray!

您或許已經知道,導覽是 Android 開發作業的基本要素。無論是使用應用程式瀏覽食譜、尋找前往喜愛餐廳的路線,還是訂餐這件最重要的事,您都很有可能需要瀏覽多個畫面的內容。在這個專案中,您會運用在單元 3 中學到的技巧,建構一個名為 Lunch Tray 的午餐訂購應用程式,並且實作檢視模型、資料繫結,以及多個畫面間的導覽功能。

以下是應用程式最終的螢幕截圖。初次啟動 Lunch Tray 應用程式時,系統會向使用者顯示歡迎畫面,內含一個「Start Order」按鈕。

20fa769d4ba93ef3.png

按一下「Start Order」後,使用者就能從可用的選項中選擇主菜。使用者可以變更所選項目,進而更新底部顯示的「Subtotal」部分。

438b61180d690b3a.png

下一個畫面可讓使用者新增配菜。

768352680759d3e2.png

之後的畫面可讓使用者選取小菜。

8ee2bf41e9844614.png

最後,系統會向使用者顯示訂單費用的摘要,並細分為小計、銷售稅和總費用。使用者也可以提交或取消訂單。

61c883c34d94b7f7.png

這兩種選項都會帶使用者返回第一個畫面。如果使用者提交了訂單,畫面底部應會顯示浮動式訊息,讓他們知道訂單已提交。

acb7d7a5d9843bac.png

3. 開始操作

下載專案程式碼

請注意,資料夾名稱是 android-basics-kotlin-lunch-tray-app。在 Android Studio 中開啟專案時,請選取這個資料夾。

  1. 前往專案所在的 GitHub 存放區頁面。
  2. 驗證分支版本名稱與程式碼研究室中指定的分支版本名稱相符。例如,在下列螢幕截圖中,分支版本名稱為「main」

1e4c0d2c081a8fd2.png

  1. 在專案的 GitHub 頁面中,按一下「Code」按鈕,畫面上會出現彈出式視窗。

1debcf330fd04c7b.png

  1. 在彈出式視窗中,按一下「Download ZIP」按鈕,將專案儲存至電腦。等待下載作業完成。
  2. 在電腦中找到該檔案 (可能位於「下載」資料夾中)。
  3. 按兩下解壓縮 ZIP 檔案。這項操作會建立含有專案檔案的新資料夾。

在 Android Studio 中開啟專案

  1. 啟動 Android Studio。
  2. 在「Welcome to Android Studio」視窗中,按一下「Open」

d8e9dbdeafe9038a.png

注意:如果 Android Studio 已開啟,請改為依序選取「File」>「Open」選單選項。

8d1fda7396afe8e5.png

  1. 在檔案瀏覽器中,前往已解壓縮的專案資料夾所在的位置 (可能位於「Downloads」資料夾中)。
  2. 按兩下該專案資料夾。
  3. 等待 Android Studio 開啟專案。
  4. 按一下「Run」按鈕 8de56cba7583251f.png,即可建構並執行應用程式。請確認應用程式的建構作業符合預期。

在開始實作 ViewModel 和導覽功能前,請花點時間確認專案已順利完成建構,並熟悉該專案。首次執行應用程式時,您會看到空白畫面。這是因為您尚未設定導覽圖,因此 MainActivity 不會顯示任何片段。

專案結構應與您處理的其他專案類似。系統會提供資料、模型和使用者介面的個別套件,以及資源的個別目錄。

a19fd8a4bc92f2fc.png

使用者可以訂購的所有午餐選項 (主餐、配菜和小菜) 會以「模型」套件的 MenuItem 類別呈現。MenuItem 物件包含名稱、說明、價格和類型。

data class MenuItem(
    val name: String,
    val description: String,
    val price: Double,
    val type: Int
) {
    fun getFormattedPrice(): String = NumberFormat.getCurrencyInstance().format(price)
}

類型是以「常數」套件中 ItemType 物件的整數呈現。

object ItemType {
    val ENTREE = 1
    val SIDE_DISH = 2
    val ACCOMPANIMENT = 3
}

您可以在資料套件的 DataSource.kt 中找到個別 MenuItem 物件。

object DataSource {
    val menuItems = mapOf(
        "cauliflower" to
        MenuItem(
            name = "Cauliflower",
            description = "Whole cauliflower, brined, roasted, and deep fried",
            price = 7.00,
            type = ItemType.ENTREE
        ),
    ...
}

這個物件只包含一個地圖,其中有索引鍵以及對應的 MenuItem。您將從 ObjectViewModel 存取 DataSource,您必須先實作 ObjectViewModel

定義 ViewModel

正如前一頁的螢幕截圖所示,應用程式會要求使用者提供以下三樣資訊:主餐、配菜和小菜。接著,訂單摘要畫面會顯示小計,並根據所選餐點計算銷售稅,然後用來算出訂單總金額。

在「模型」套件中開啟 OrderViewModel.kt,您就會看到幾個已經定義的變數。menuItems 屬性可讓您從 ViewModel 存取 DataSource

val menuItems = DataSource.menuItems

首先,previousEntreePricepreviousSidePricepreviousAccompanimentPrice 也有一些變數。小計會在使用者做出選擇時更新 (而不是在最後加總),因此如果使用者在前往下一個畫面之前變更了所選項目,系統就會透過這些變數來追蹤使用者先前的選項。這些變數可確保小計反映了先前和目前選取項目的價差。

private var previousEntreePrice = 0.0
private var previousSidePrice = 0.0
private var previousAccompanimentPrice = 0.0

此外,還有 _entree_side_accompaniment 這類私有變數,可用於儲存目前選取的選項。這些都屬於 MutableLiveData<MenuItem?> 類型。每個類型都隨附公開備份屬性 entreesideaccompaniment (屬於不可變動的 LiveData<MenuItem?> 類型)。您可以透過片段的版面配置來存取這些內容,讓所選項目顯示在畫面上。LiveData 物件中包含的 MenuItem 也可以是空值,因為使用者也可以不選取主餐、配菜和/或小菜。

// 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

小計、總計與稅金也有 LiveData 變數,其採數字格式設定,因此能以貨幣形式顯示。

// 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)
}

最後,稅率是硬式編碼的 0.08 (8%)。

private val taxRate = 0.08

您必須實作 OrderViewModel 中的六個方法。

setEntree()、setSide() 和 setAccompaniment()

這些方法應該以相同方式適用於主餐、配菜和小菜。舉例來說,setEntree() 應執行以下操作:

  1. 如果 _entree 不是 null (也就是使用者已選取主餐,但後來變更了選項),請將 previousEntreePrice 設為 current _entree 的價格。
  2. 如果 _subtotalnull,請從小計減去 previousEntreePrice
  3. _entree 的值更新為傳遞至函式的主餐 (使用 menuItems 存取 MenuItem)。
  4. 呼叫 updateSubtotal(),傳遞新選取的主餐價格。

setSide()setAccompaniment() 的邏輯與 setEntree() 的實作相同。

updateSubtotal()

系統會呼叫 updateSubtotal(),並加上應加入小計的新價格引數。這個方法需要執行以下三件事:

  1. 如果 _subtotal 不是 null,請將 itemPrice 新增至 _subtotal
  2. 如果 _subtotalnull,請將 _subtotal 設為 itemPrice
  3. 設定或更新 _subtotal 後,呼叫 calculateTaxAndTotal() 即可更新這些值,以反映新的小計。

calculateTaxAndTotal()

calculateTaxAndTotal() 應根據小計來更新稅金的變數和總金額。實作如下方法:

  1. _tax 設為稅率乘上小計。
  2. _total 設為小計加上稅金。

resetOrder()

使用者提交或取消訂單時,系統會呼叫 resetOrder()。當使用者建立新訂單時,請確保應用程式不會留下任何資料。

建議您將在 OrderViewModel 修改的所有變數設回原始值 (0.0 或空值),藉此實作 resetOrder()

建立資料繫結變數

在版面配置檔案中實作資料繫結。開啟版面配置檔案,並新增 OrderViewModel 類型和/或對應片段類別的資料繫結變數。

您需要實作所有 TODO 註解,才能在四個版面配置檔案中設定文字和點擊事件監聽器:

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

系統會在版面配置檔案中的 TODO 註解標示每個特定工作,步驟摘要如下。

  1. fragment_entree_menu.xml<data> 標記中,新增 EntreeMenuFragment 的繫結變數。對於每個圓形按鈕,您需在按鈕已選取的情況下,於 ViewModel 中設定主餐。小計文字檢視畫面的文字應隨之更新。此外,您也需設定 cancel_buttonnext_buttononClick 屬性,以便分別取消訂單或前往下一個畫面。
  2. fragment_side_menu.xml 中執行相同操作,新增 SideMenuFragment 的繫結變數,但在點選每個圓形按鈕時,於檢視模型中設定配菜。小計文字也會需要更新,而您也需為取消和下一步按鈕設定 onClick 屬性。
  3. 再次執行相同的操作,但在 fragment_accompaniment_menu.xml 中,這次使用 AccompanimentMenuFragment 的繫結變數,在每個圓形按鈕皆已選取時設定小菜。此外,您也需設定小計文字、取消按鈕和下一步按鈕的屬性。
  4. fragment_checkout.xml 中,您需要新增 <data> 標記,以便定義繫結變數。而在 <data> 標記內,請新增兩個繫結變數:一個用於 OrderViewModel,另一個用於 CheckoutFragment。在文字檢視區塊中,您需從 OrderViewModel 設定所選主餐、配菜和小菜的名稱與價格。您還需要設定 OrderViewModel 中的小計、稅金和總金額。接著,使用 CheckoutFragment 中的適當函式,設定訂單提交和取消時的 onClickAttributes

初始化片段中的資料繫結變數

初始化 onViewCreated() 方法中對應片段檔案內的資料繫結變數。

  1. EntreeMenuFragment
  2. SideMenuFragment
  3. AccompanimentMenuFragment
  4. CheckoutFragment

建立導覽圖

單元 3 中已說明,活動中的 FragmentContainerView 會代管導覽圖。開啟 activity_main.xml 並使用以下程式碼來取代 TODO,以宣告 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" />

mobile_navigation.xml 導覽圖位於 res.navigation 套件中。

e3381215c35c1726.png

這是應用程式的導覽圖,但此檔案目前為空白。您的工作是為導覽圖新增目的地,並建立以下在不同畫面之間導覽的模型。

  1. StartOrderFragment 前往 EntreeMenuFragment
  2. EntreeMenuFragment 前往 SideMenuFragment
  3. SideMenuFragment 前往 AccompanimentMenuFragment
  4. AccompanimentMenuFragment 前往 CheckoutFragment
  5. CheckoutFragment 前往 StartOrderFragment
  6. EntreeMenuFragment 前往 StartOrderFragment
  7. SideMenuFragment 前往 StartOrderFragment
  8. AccompanimentMenuFragment 前往 StartOrderFragment
  9. 起始目的地 應為 StartOrderFragment

設定導覽圖後,您需在片段類別中執行導覽。在片段中實作剩餘的 TODOMainActivity.kt 註解。

  1. 針對 EntreeMenuFragmentSideMenuFragmentAccompanimentMenuFragment 中的 goToNextScreen() 方法,前往應用程式中的下一個畫面。
  2. 針對 EntreeMenuFragmentSideMenuFragmentAccompanimentMenuFragmentCheckoutFragment 中的 cancelOrder() 方法,首先在 sharedViewModel 上呼叫 resetOrder(),然後前往 StartOrderFragment
  3. StartOrderFragment 中,實作 setOnClickListener() 以前往 EntreeMenuFragment
  4. CheckoutFragment 中實作 submitOrder() 方法。在 sharedViewModel 上呼叫 resetOrder(),然後前往 StartOrderFragment
  5. 最後在 MainActivity.kt 中,將 NavHostFragmentnavController 設為 navController

4. 測試應用程式

Lunch Tray 專案包含一個「androidTest」目標,有多種測試案例:MenuContentTestsNavigationTestsOrderFunctionalityTests

執行測試

如要執行測試,您可以執行下列其中一項操作:

若是單一測試案例,請開啟測試案例類別,然後按一下類別宣告左側的綠色箭頭。接著從選單中選取「Run」選項。這麼做將會執行測試案例中的所有測試。

8ddcbafb8ec14f9b.png

您通常只需要執行一項測試,例如在只有一個測試失敗,而其他測試都通過時。執行單一測試的做法,與執行整個測試案例一樣。請按一下綠色箭頭,並選取「Run」選項。

335664b7fc8b4fb5.png

如果您有多個測試案例,也可以執行整個測試套件。就像執行應用程式一樣,您可以在「Run」選單中找到這個選項。

80312efedf6e4dd3.png

請注意,Android Studio 預設會執行您執行的最後一個目標 (應用程式、測試目標等),因此如果選單仍顯示「Run」>「Run ‘app'」,您可以依序選取「Run」>「Run」執行測試目標。

95aacc8f749dee8e.png

然後從彈出式選單中選擇測試目標。

8b702efbd4d21d3d.png

5. 選填:請提供您的意見回饋。

我們很樂意聆聽您對這個專案的意見。請填寫這份簡短的問卷調查,讓我們瞭解您的意見,這將有助於我們規劃此課程日後的專案。