建構一款名為「Lunch Tray」的新應用程式
建構項目
您會建構一個名為 Lunch Tray 的訂餐應用程式、透過 data binding 實作 ViewModel
,並在 fragment 之間加入 navigation 功能。
專案說明
本程式碼研究室將說明如何自行建構一款名為「Lunch Tray」的新應用程式。我們會引導您逐步完成 Lunch Tray 應用程式專案,包括在 Android Studio 中設定和測試專案。
本程式碼研究室與本課程中的其他部分不同。與先前的程式碼研究室不同,本程式碼研究室的目的並不是逐步說明如何建構應用程式,而是設定將由您獨力完成的專案,提供自行完成應用程式及檢查工作成果的相關指示。
我們改為一併在您將下載的應用程式中提供測試套件,而非程式碼解答。您將在 Android Studio 中執行這些測試 (本程式碼研究室稍後會說明操作方法),並查看程式碼是否通過測試。這可能需要多試幾次,即使是專業開發人員也很難第一次嘗試就通過所有測試!程式碼通過所有測試後,您就能將這項專案視為完成。
我們瞭解,您可能只是想獲得解答來對照檢查。我們特意不提供程式碼解答,是因為希望您能透過練習,體驗專業開發人員的作業環境。您可能會需要用到較不嫻熟的其他技能,例如:
- 在 Google 上搜尋您在應用程式中不認得的字詞、錯誤訊息和程式碼片段。
- 測試程式碼、解讀錯誤,然後變更程式碼並重複測試。
- 回去閱讀先前 Android 基本概念中的內容,溫故知新。
- 將您知道可順利執行的程式碼 (例如專案內提供的程式碼,或是單元 3 中其他應用程式先前的解決方案程式碼) 與您編寫的程式碼進行比對。
乍看之下可能很困難,但我們百分之百相信如果您能夠完成單元 3,就已經對這項專案做好準備了。請按照自己的步調進行,不要放棄,我們對您有信心。
完成的應用程式總覽
歡迎來到專案:Lunch Tray!
您或許已經知道,navigation 是 Android 開發作業的基本要素。無論是使用應用程式瀏覽食譜、尋找前往喜愛餐廳的路線,還是訂餐這件最重要的事,您都很有可能需要瀏覽多個畫面的內容。在這個專案中,您會運用在單元 3 中學到的技巧,建構一個名為 Lunch Tray 的午餐訂購應用程式,並且實作 view model、data binding,以及多個畫面間的 navigation 功能。
以下是應用程式最終的螢幕截圖。初次啟動 Lunch Tray 應用程式時,系統會向使用者顯示歡迎畫面,內含一個「Start Order」按鈕。

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

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

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

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

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

開始操作
下載專案程式碼
請注意,資料夾名稱是 android-basics-kotlin-lunch-tray-app
。在 Android Studio 中開啟專案時,請選取這個資料夾。
在開始實作 ViewModel
和 navigation 功能前,請花點時間確認專案已順利完成建構,並熟悉該專案。首次執行應用程式時,您會看到空白畫面。這是因為您尚未設定 navigation graph,因此 MainActivity
不會顯示任何 fragment。
專案結構應與您處理的其他專案類似。系統會提供 data、model和 ui 的個別 package,以及 resource 的個別目錄。

data class MenuItem
使用者可以訂購的所有午餐選項 (主餐、配菜和小菜) 會以「model」package 的 MenuItem
類別呈現。MenuItem
物件包含菜品名稱(name)、菜品說明(description)、價格(price)和菜品類型(type)。
1 | data class MenuItem( |
object ItemType
type 是以「constants」package 中 ItemType
物件的 Int 呈現。
1 | object ItemType { |
object DataSource
您可以在 data package 的 DataSource.kt
中找到個別 MenuItem
物件。
1 | object DataSource { |
這個物件只包含一個 map,其中有索引 key 以及對應的 value MenuItem
。您將從 ObjectViewModel
存取 DataSource
,您必須先實作 ObjectViewModel
。
ViewModel - class OrderViewModel
正如前一頁的螢幕截圖所示,應用程式會要求使用者提供以下三樣資訊:主餐、配菜和小菜。接著,訂單摘要畫面會顯示小計,並根據所選餐點計算銷售稅,然後用來算出訂單總金額。
在「model」package 中開啟 OrderViewModel.kt
,您就會看到幾個已經定義的變數。menuItems
屬性可讓您從 ViewModel
存取 DataSource
。
1 | // DataSource 中 menuItems 的 Map |
首先,previousEntreePrice
、previousSidePrice
和 previousAccompanimentPrice
也有一些變數。小計會在使用者做出選擇時更新 (而不是在最後加總),因此如果使用者在前往下一個畫面之前變更了所選項目,系統就會透過這些變數來追蹤使用者先前的選項。這些變數可確保小計反映了先前和目前選取項目的價差。
1 | private var previousEntreePrice = 0.0 // 先前主餐價格 |
此外,還有 _entree
、_side
和 _accompaniment
這類 private 變數,可用於儲存目前選取的選項。這些都屬於 MutableLiveData<MenuItem?>
類型。每個類型都伴隨 public backing 屬性 entree
、side
和 accompaniment
(屬於不可變動的 LiveData<MenuItem?>
類型)。您可以透過 fragment 的 layout 來存取這些內容,讓所選 item 顯示在畫面上。LiveData
物件中包含的 MenuItem
也可以是空值,因為使用者也可以不選取主餐、配菜和/或小菜。
1 | // 使用者選取的主餐 |
小計、總計與稅金也有 LiveData
變數,其採數字格式(number formatting)設定,因此能以貨幣形式顯示。
1 | // 訂單小計(預設0.0元) |
最後,tax rate 是硬式編碼(hardcoded)的 0.08 (8%)。
1 | private val taxRate = 0.08 |
您必須實作 OrderViewModel
中的六個方法。
■ setEntree()
、setSide()
和 setAccompaniment()
這些方法應該以相同方式適用於主餐、配菜和小菜。舉例來說,setEntree()
應執行以下操作:
- 如果
_entree
不是null
(也就是使用者已選取主餐,但後來變更了選項),請將previousEntreePrice
設為current _entree
的價格。 - 如果
_subtotal
不是null
,請從 subtotal 減去previousEntreePrice
。 - 將
_entree
的值更新為傳遞至函式的 entree (使用menuItems
存取MenuItem
)。 - 呼叫
updateSubtotal()
,傳遞新選取的主餐價格。
setSide()
和 setAccompaniment()
的邏輯與 setEntree()
的實作相同。
■ updateSubtotal()
系統會呼叫 updateSubtotal()
,並加上應加入小計的新價格參數。這個方法需要執行以下三件事:
- 如果
_subtotal
不是null
,請將itemPrice
新增至_subtotal
。 - 如果
_subtotal
是null
,請將_subtotal
設為itemPrice
。 - 設定或更新
_subtotal
後,呼叫calculateTaxAndTotal()
即可更新這些值,以反映新的小計。
■ calculateTaxAndTotal()
calculateTaxAndTotal()
應根據小計(subtotal)來更新稅金(tax)的變數和總金額(total)。實作如下方法:
- 將
_tax
設為 tax rate 乘上 subtotal。 - 將
_total
設為 subtotal 加上 tax。
■ resetOrder()
使用者提交或取消訂單時,系統會呼叫 resetOrder()
。當使用者建立新訂單時,請確保應用程式不會留下任何資料。
建議您將在 OrderViewModel
修改的所有變數設回原始值 (0.0 或空值),藉此實作 resetOrder()
。
建立 data binding 變數
在 layout 檔案中實作 data binding。開啟 layout 檔案,並新增 OrderViewModel
type 和/或對應 fragment class 的 data binding 變數。
您需要實作所有 TODO
註解,才能在四個 layout 檔案中設定 text 和 click listeners:
fragment_entree_menu.xml
fragment_side_menu.xml
fragment_accompaniment_menu.xml
fragment_checkout.xml
系統會在 layout 檔案中的 TODO
註解標示每個特定工作,步驟摘要如下。
在
fragment_entree_menu.xml
的<data>
標記中,新增EntreeMenuFragment
的 binding 變數。對於每個 radio button,您需在按鈕已選取的情況下,於ViewModel
中設定主餐。subtotal text view 的 text 應隨之更新。此外,您也需設定cancel_button
和next_button
的onClick
屬性,以便分別取消訂單或前往下一個畫面。在
fragment_side_menu.xml
中執行相同操作,新增SideMenuFragment
的 binding 變數,但在點選每個 radio button 時,於 view model 中設定配菜。subtotal text 也會需要更新,而您也需為 cancel 和 next button 設定onClick
屬性。再次執行相同的操作,但在
fragment_accompaniment_menu.xml
中,這次使用AccompanimentMenuFragment
的 binding 變數,在每個 radio button 皆已選取時設定小菜。此外,您也需設定 subtotal text、cancel button 和 next button 的屬性。在
fragment_checkout.xml
中,您需要新增<data>
標記,以便定義 binding 變數。而在<data>
標記內,請新增兩個 binding 變數:一個用於OrderViewModel
,另一個用於CheckoutFragment
。在 text view 中,您需從OrderViewModel
設定所選主餐、配菜和小菜的名稱與價格。您還需要設定OrderViewModel
中的小計、稅金和總金額。接著,使用CheckoutFragment
中的適當函式,設定訂單提交和取消時的onClickAttributes
。
初始化 fragment 中的 data binding 變數
初始化 onViewCreated()
方法中對應 fragment 檔案內的 data binding 變數。
EntreeMenuFragment
SideMenuFragment
AccompanimentMenuFragment
CheckoutFragment
建立 navigation graph
單元 3 中已說明,activity 中的 FragmentContainerView
會代管 navigation graph。開啟 activity_main.xml
並使用以下程式碼來取代 TODO
,以宣告 FragmentContainerView
。
1 | <androidx.fragment.app.FragmentContainerView |
mobile_navigation.xml
navigation graph 位於 res/navigation
package 中。

這是應用程式的 navigation graph,但此檔案目前為空白。您的工作是為 navigation graph 新增目的地,並建立以下在不同畫面之間導覽的模型。
- 從
StartOrderFragment
前往EntreeMenuFragment
- 從
EntreeMenuFragment
前往SideMenuFragment
- 從
SideMenuFragment
前往AccompanimentMenuFragment
- 從
AccompanimentMenuFragment
前往CheckoutFragment
- 從
CheckoutFragment
前往StartOrderFragment
- 從
EntreeMenuFragment
前往StartOrderFragment
- 從
SideMenuFragment
前往StartOrderFragment
- 從
AccompanimentMenuFragment
前往StartOrderFragment
- 起始目的地 應為
StartOrderFragment
設定 navigation graph 後,您需在 fragment 類別中執行導覽。在 fragment 中實作剩餘的 TODO
和 MainActivity.kt
註解。
- 針對
EntreeMenuFragment
、SideMenuFragment
和AccompanimentMenuFragment
中的goToNextScreen()
方法,前往應用程式中的下一個畫面。 - 針對
EntreeMenuFragment
、SideMenuFragment
、AccompanimentMenuFragment
和CheckoutFragment
中的cancelOrder()
方法,首先在sharedViewModel
上呼叫resetOrder()
,然後前往StartOrderFragment
。 - 在
StartOrderFragment
中,實作setOnClickListener()
以前往EntreeMenuFragment
。 - 在
CheckoutFragment
中實作submitOrder()
方法。在sharedViewModel
上呼叫resetOrder()
,然後前往StartOrderFragment
。 - 最後在
MainActivity.kt
中,將NavHostFragment
的navController
設為navController
。
完成結果
完成後的 Lunch Tray App 執行結果如下:
測試應用程式
Lunch Tray 專案包含一個「androidTest」目標,有多種測試案例:MenuContentTests
、NavigationTests
和 OrderFunctionalityTests
。
執行測試
如要執行測試,您可以執行下列其中一項操作:
若是單一測試案例,請開啟測試案例類別,然後按一下類別宣告左側的綠色箭頭。接著從選單中選取「Run」選項。這麼做將會執行測試案例中的所有測試。

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

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

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

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

測試結果
MenuContentTests
NavigationTests
OrderFunctionalityTests