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」按鈕。
按一下「Start Order」後,使用者就能從可用的選項中選擇主菜。使用者可以變更所選項目,進而更新底部顯示的「Subtotal」部分。
下一個畫面可讓使用者新增配菜。
之後的畫面可讓使用者選取小菜。
最後,系統會向使用者顯示訂單費用的摘要,並細分為小計、銷售稅和總費用。使用者也可以提交或取消訂單。
這兩種選項都會帶使用者返回第一個畫面。如果使用者提交了訂單,畫面底部應會顯示浮動式訊息,讓他們知道訂單已提交。
3. 開始操作
下載專案程式碼
請注意,資料夾名稱是 android-basics-kotlin-lunch-tray-app
。在 Android Studio 中開啟專案時,請選取這個資料夾。
- 前往專案所在的 GitHub 存放區頁面。
- 驗證分支版本名稱與程式碼研究室中指定的分支版本名稱相符。例如,在下列螢幕截圖中,分支版本名稱為「main」。
- 在專案的 GitHub 頁面中,按一下「Code」按鈕,畫面上會出現彈出式視窗。
- 在彈出式視窗中,按一下「Download ZIP」按鈕,將專案儲存至電腦。等待下載作業完成。
- 在電腦中找到該檔案 (可能位於「下載」資料夾中)。
- 按兩下解壓縮 ZIP 檔案。這項操作會建立含有專案檔案的新資料夾。
在 Android Studio 中開啟專案
- 啟動 Android Studio。
- 在「Welcome to Android Studio」視窗中,按一下「Open」。
注意:如果 Android Studio 已開啟,請改為依序選取「File」>「Open」選單選項。
- 在檔案瀏覽器中,前往已解壓縮的專案資料夾所在的位置 (可能位於「Downloads」資料夾中)。
- 按兩下該專案資料夾。
- 等待 Android Studio 開啟專案。
- 按一下「Run」按鈕 ,即可建構並執行應用程式。請確認應用程式的建構作業符合預期。
在開始實作 ViewModel
和導覽功能前,請花點時間確認專案已順利完成建構,並熟悉該專案。首次執行應用程式時,您會看到空白畫面。這是因為您尚未設定導覽圖,因此 MainActivity
不會顯示任何片段。
專案結構應與您處理的其他專案類似。系統會提供資料、模型和使用者介面的個別套件,以及資源的個別目錄。
使用者可以訂購的所有午餐選項 (主餐、配菜和小菜) 會以「模型」套件的 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()
應執行以下操作:
- 如果
_entree
不是null
(也就是使用者已選取主餐,但後來變更了選項),請將previousEntreePrice
設為current _entree
的價格。 - 如果
_subtotal
是null
,請從小計減去previousEntreePrice
。 - 將
_entree
的值更新為傳遞至函式的主餐 (使用menuItems
存取MenuItem
)。 - 呼叫
updateSubtotal()
,傳遞新選取的主餐價格。
setSide()
和 setAccompaniment()
的邏輯與 setEntree()
的實作相同。
updateSubtotal()
系統會呼叫 updateSubtotal()
,並加上應加入小計的新價格引數。這個方法需要執行以下三件事:
- 如果
_subtotal
不是null
,請將itemPrice
新增至_subtotal
。 - 如果
_subtotal
是null
,請將_subtotal
設為itemPrice
。 - 設定或更新
_subtotal
後,呼叫calculateTaxAndTotal()
即可更新這些值,以反映新的小計。
calculateTaxAndTotal()
calculateTaxAndTotal()
應根據小計來更新稅金的變數和總金額。實作如下方法:
- 將
_tax
設為稅率乘上小計。 - 將
_total
設為小計加上稅金。
resetOrder()
使用者提交或取消訂單時,系統會呼叫 resetOrder()
。當使用者建立新訂單時,請確保應用程式不會留下任何資料。
建議您將在 OrderViewModel
修改的所有變數設回原始值 (0.0 或空值),藉此實作 resetOrder()
。
建立資料繫結變數
在版面配置檔案中實作資料繫結。開啟版面配置檔案,並新增 OrderViewModel
類型和/或對應片段類別的資料繫結變數。
您需要實作所有 TODO
註解,才能在四個版面配置檔案中設定文字和點擊事件監聽器:
fragment_entree_menu.xml
fragment_side_menu.xml
fragment_accompaniment_menu.xml
fragment_checkout.xml
系統會在版面配置檔案中的 TODO 註解標示每個特定工作,步驟摘要如下。
- 在
fragment_entree_menu.xml
的<data>
標記中,新增EntreeMenuFragment
的繫結變數。對於每個圓形按鈕,您需在按鈕已選取的情況下,於ViewModel
中設定主餐。小計文字檢視畫面的文字應隨之更新。此外,您也需設定cancel_button
和next_button
的onClick
屬性,以便分別取消訂單或前往下一個畫面。 - 在
fragment_side_menu.xml
中執行相同操作,新增SideMenuFragment
的繫結變數,但在點選每個圓形按鈕時,於檢視模型中設定配菜。小計文字也會需要更新,而您也需為取消和下一步按鈕設定onClick
屬性。 - 再次執行相同的操作,但在
fragment_accompaniment_menu.xml
中,這次使用AccompanimentMenuFragment
的繫結變數,在每個圓形按鈕皆已選取時設定小菜。此外,您也需設定小計文字、取消按鈕和下一步按鈕的屬性。 - 在
fragment_checkout.xml
中,您需要新增<data>
標記,以便定義繫結變數。而在<data>
標記內,請新增兩個繫結變數:一個用於OrderViewModel
,另一個用於CheckoutFragment
。在文字檢視區塊中,您需從OrderViewModel
設定所選主餐、配菜和小菜的名稱與價格。您還需要設定OrderViewModel
中的小計、稅金和總金額。接著,使用CheckoutFragment
中的適當函式,設定訂單提交和取消時的onClickAttributes
。
。
初始化片段中的資料繫結變數
初始化 onViewCreated()
方法中對應片段檔案內的資料繫結變數。
EntreeMenuFragment
SideMenuFragment
AccompanimentMenuFragment
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 套件中。
這是應用程式的導覽圖,但此檔案目前為空白。您的工作是為導覽圖新增目的地,並建立以下在不同畫面之間導覽的模型。
- 從
StartOrderFragment
前往EntreeMenuFragment
- 從
EntreeMenuFragment
前往SideMenuFragment
- 從
SideMenuFragment
前往AccompanimentMenuFragment
- 從
AccompanimentMenuFragment
前往CheckoutFragment
- 從
CheckoutFragment
前往StartOrderFragment
- 從
EntreeMenuFragment
前往StartOrderFragment
- 從
SideMenuFragment
前往StartOrderFragment
- 從
AccompanimentMenuFragment
前往StartOrderFragment
- 起始目的地 應為
StartOrderFragment
設定導覽圖後,您需在片段類別中執行導覽。在片段中實作剩餘的 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
。
4. 測試應用程式
Lunch Tray 專案包含一個「androidTest」目標,有多種測試案例:MenuContentTests
、NavigationTests
和 OrderFunctionalityTests
。
執行測試
如要執行測試,您可以執行下列其中一項操作:
若是單一測試案例,請開啟測試案例類別,然後按一下類別宣告左側的綠色箭頭。接著從選單中選取「Run」選項。這麼做將會執行測試案例中的所有測試。
您通常只需要執行一項測試,例如在只有一個測試失敗,而其他測試都通過時。執行單一測試的做法,與執行整個測試案例一樣。請按一下綠色箭頭,並選取「Run」選項。
如果您有多個測試案例,也可以執行整個測試套件。就像執行應用程式一樣,您可以在「Run」選單中找到這個選項。
請注意,Android Studio 預設會執行您執行的最後一個目標 (應用程式、測試目標等),因此如果選單仍顯示「Run」>「Run ‘app'」,您可以依序選取「Run」>「Run」執行測試目標。
然後從彈出式選單中選擇測試目標。
5. 選填:請提供您的意見回饋。
我們很樂意聆聽您對這個專案的意見。請填寫這份簡短的問卷調查,讓我們瞭解您的意見,這將有助於我們規劃此課程日後的專案。