專案: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

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

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

此外,還有 _entree、_side 和 _accompaniment 這類私有變數,可用於儲存目前選取的選項。這些都屬於 MutableLiveData<MenuItem?> 類型。每個類型都隨附公開備份屬性 entree、side 和 accompaniment (屬於不可變動的 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. 如果 _subtotal 是 null,請從小計減去 previousEntreePrice。
  3. 將 _entree 的值更新為傳遞至函式的主餐 (使用 menuItems 存取 MenuItem)。
  4. 呼叫 updateSubtotal(),傳遞新選取的主餐價格。

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

updateSubtotal()

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

  1. 如果 _subtotal 不是 null,請將 itemPrice 新增至 _subtotal。
  2. 如果 _subtotal 是 null,請將 _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_button 和 next_button 的 onClick 屬性,以便分別取消訂單或前往下一個畫面。
  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

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

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

4. 測試應用程式

Lunch Tray 專案包含一個「androidTest」目標,有多種測試案例:MenuContentTests、NavigationTests 和 OrderFunctionalityTests。

執行測試

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

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

8ddcbafb8ec14f9b.png

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

335664b7fc8b4fb5.png

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

80312efedf6e4dd3.png

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

95aacc8f749dee8e.png

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

8b702efbd4d21d3d.png

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

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