專案: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」(下載 ZIP) 按鈕,將專案儲存至電腦。等待下載作業完成。
  2. 在電腦上尋找檔案 (可能位於「Downloads」(下載) 資料夾中)。
  3. 按兩下 ZIP 檔案,將檔案解壓縮。這項操作會建立含有專案檔案的新資料夾。

在 Android Studio 中開啟專案

  1. 啟動 Android Studio。
  2. 在「Welcome to Android Studio」(歡迎使用 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" />

已在 res.navigation 套件中找到 mobile_navigation.xml 導覽圖。

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. 選填:請提供您的意見回饋。

我們想瞭解您對這個專案的意見。請填寫這份簡短的問卷調查,以提供您的意見回饋。您的意見將有助於我們在日後規劃這個課程中的專案。