1. 事前準備
您已學會如何使用活動、片段、意圖、資料繫結、導覽元件,也瞭解架構元件的基本概念。在本程式碼研究室中,您必須集中統整所有工作,並處理杯子蛋糕訂購應用程式,這是一項進階範例。
您將瞭解如何使用共用 ViewModel
在相同活動的片段之間共用資料,還有 LiveData
轉換等新概念。
必要條件
- 能讀懂且理解 XML 格式的 Android 版面配置
- 熟悉 Jetpack Navigation 元件的基本概念
- 能在應用程式中建立含有片段目的地的導覽圖
- 曾在活動中使用片段
- 知道如何建立
ViewModel
來儲存應用程式資料 - 會搭配使用
LiveData
與資料繫結,以ViewModel
中的應用程式資料隨時更新 UI
課程內容
- 如何在更進階用途中導入建議的應用程式架構做法
- 如何在活動的各個片段中使用共用的
ViewModel
- 如何套用
LiveData
轉換
建構項目
- 此杯子蛋糕應用程式會顯示杯子蛋糕的訂單流程,讓使用者選擇杯子蛋糕口味、數量和取貨日期。
需求條件
- 已安裝 Android Studio 的電腦。
- 杯子蛋糕應用程式的範例程式碼。
2. 範例應用程式總覽
杯子蛋糕應用程式總覽
杯子蛋糕應用程式示範如何設計及導入線上訂購應用程式。本課程結束時,您將完成有下列畫面的杯子蛋糕應用程式。使用者可以選擇杯子蛋糕訂單的數量、口味和其他選項。
下載本程式碼研究室的範例程式碼
本程式碼研究室提供範例程式碼,讓您以本程式碼研究室所教授的功能擴充應用程式。範例程式碼含有您先前在程式碼研究室中熟悉的程式碼。
請注意,如果您從 GitHub 下載範例程式碼,專案的資料夾名稱會是 android-basics-kotlin-cupcake-app-starter
。在 Android Studio 中開啟專案時,請選取此資料夾。
如要取得本程式碼研究室的程式碼,並在 Android Studio 中開啟,請按照下列步驟操作。
取得程式碼
- 按一下上面顯示的網址。系統會在瀏覽器中開啟專案的 GitHub 頁面。
- 在專案的 GitHub 頁面中,按一下「Code」按鈕開啟對話方塊。
- 在對話方塊中,按一下「Download ZIP」按鈕,將專案儲存到電腦。等待下載作業完成。
- 在電腦中找到該檔案 (可能位於「下載」資料夾中)。
- 按兩下解壓縮 ZIP 檔案。這項操作會建立含有專案檔案的新資料夾。
在 Android Studio 中開啟專案
- 啟動 Android Studio。
- 在「Welcome to Android Studio」視窗中,按一下「Open an existing Android Studio project」。
注意:如果 Android Studio 已開啟,請依序選取「File」>「New」>「Import Project」選單選項。
- 在「Import Project」對話方塊中,前往解壓縮專案資料夾所在的位置 (可能位於「下載」資料夾中)。
- 按兩下該專案資料夾。
- 等待 Android Studio 開啟專案。
- 按一下「Run」按鈕 即可建構並執行應用程式。請確認應用程式的建構符合預期。
- 在「Project」工具視窗中瀏覽專案檔案,查看應用程式的設定方式。
範例程式碼逐步操作說明
- 在 Android Studio 中開啟已下載的專案。專案的資料夾名稱為
android-basics-kotlin-cupcake-app-starter
。然後執行應用程式。 - 瀏覽檔案以瞭解範例程式碼。針對版面配置檔案,您可以使用右上角的「Split」選項,同時查看版面配置和 XML 的預覽畫面。
- 編譯並執行應用程式時,您會發現應用程式並不完整。除了顯示
Toast
訊息外,這些按鈕的作用不大,而且無法導覽至其他片段。
以下說明專案裡的重要檔案。
MainActivity:
MainActivity
和預設產生的程式碼相似,用於將活動的內容檢視設定為 activity_main.xml
。此程式碼使用參數化建構函式 AppCompatActivity(@LayoutRes int contentLayoutId)
,其版面配置會加載為 super.onCreate(savedInstanceState)
的一部分。
MainActivity
類別的程式碼
class MainActivity : AppCompatActivity(R.layout.activity_main)
與使用預設 AppCompatActivity
建構函式的下列程式碼相同:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
版面配置 (res/layout 資料夾):
layout
資源資料夾含有活動和片段版面配置檔案。這些是較簡單的版面配置檔案,而在先前的程式碼研究室中已熟悉 XML。
fragment_start.xml
是應用程式中顯示的第一個畫面。這項產品提供杯子蛋糕圖片和三個按鈕,可供選擇想要訂購的杯子數量:一個杯子蛋糕、六個杯子蛋糕和十二個杯子蛋糕。fragment_flavor.xml
會顯示以圓形按鈕選項呈現的杯子蛋糕口味清單,以及「Next」按鈕。fragment_pickup.xml
提供選擇取貨日的選項,按一下「Next」按鈕即可前往摘要畫面。fragment_summary.xml
會顯示訂單詳細資料摘要,例如數量、口味,以及可將訂單傳送至其他應用程式的按鈕。
片段類別:
StartFragment.kt
是應用程式中顯示的第一個畫面。此類別含有三個按鈕的檢視繫結程式碼和點擊處理常式。FlavorFragment.kt
、PickupFragment.kt
和SummaryFragment.kt
類別主要包含樣板程式碼,以及「Next」或「Send Order to Another App」按鈕的按鈕點擊處理常式,且會顯示浮動式訊息。
資源 (res 資料夾):
drawable
資料夾包含第一個畫面的杯子蛋糕素材資源,以及啟動器圖示檔案。navigation/nav_graph.xml
包含四個片段目的地 (startFragment
、flavorFragment
、pickupFragment
和summaryFragment
),但不含「動作」,您稍後才會在程式碼研究室中加以定義。values
資料夾包含顏色、維度、字串、樣式和用於自訂應用程式佈景主題的佈景主題。建議您從先前的程式碼研究室熟悉相關資源型別。
3. 完成導覽圖
在這項工作中,我們將連結杯子蛋糕應用程式的畫面,並在應用程式中導入適當的導覽功能。
您記得必須使用導覽元件嗎?請按照這份指南中的複習,瞭解如何設定專案和應用程式,目標是:
- 加入 Jetpack Navigation 程式庫
- 在活動中新增
NavHost
- 建立導覽圖
- 在導覽圖中新增片段目的地
在導覽圖中連接目的地
- 在 Android Studio 的「Project」視窗中,開啟 res > navigation > nav_graph.xml 檔案。如果尚未選取「Design」分頁標籤,請予以選取。
- 選取後即可開啟「Navigation Editor」,以視覺化方式呈現應用程式中的導覽圖。您應該會看到應用程式中已有四個片段。
- 連結導覽圖中的片段目的地。建立從
startFragment
到flavorFragment
的動作、從flavorFragment
到pickupFragment
的連線,以及從pickupFragment
到summaryFragment
的連線。如需更詳細的操作說明,請按照下列後續步驟操作。 - 將滑鼠游標懸停在 startFragment 上,直到片段周圍顯示灰色框線,且片段右側邊緣的中心顯示灰色圓圈圖示。按一下圓圈並拖曳至 flavorFragment,然後放開滑鼠。
- 兩個片段之間的箭頭表示已成功連線,因此您將可以從 startFragment 前往 flavorFragment。這就是「導覽」動作,您可以在先前的程式碼研究室中學到。
- 同樣地,將新增從 flavorFragment 到 pickupFragment,以及從 pickupFragment 到 summaryFragment 的導覽動作。建立導覽動作後,完成的導覽圖看起來會如下所示。
- 您建立的三個新動作應該也會顯示在「Component Tree」窗格中。
- 定義導覽圖時,建議您也指定開始目的地。目前您可以看到 startFragment 旁有一個小型房屋圖示。
這表示 startFragment 將是第一個顯示在 NavHost
中的片段。設定為應用程式所需的預期行為。日後只要在任一片段上按一下滑鼠右鍵,然後選取「Set as Start Destination」選單選項,即可隨時變更起點的位置。
從起始片段前往口味片段
接下來,您必須新增程式碼,目的是在第一個片段中輕觸按鈕時從 startFragment 前往 flavorFragment,而非顯示 Toast
訊息。以下是起始片段版面配置的參考資料。您在之後的任務中,必須將杯子蛋糕的數量傳遞至口味片段。
- 在「Project」視窗中,依序開啟 app > java > com.example.cupcake > StartFragment Kotlin 檔案。
- 在
onViewCreated()
方法中,請留意,點擊事件監聽器設定在三個按鈕上。輕觸各個按鈕時,系統會呼叫orderCupcake()
方法,並以杯子蛋糕數量 (1、6 或 12 個杯子蛋糕) 做為參數的數量。
參考識別碼:
orderOneCupcake.setOnClickListener { orderCupcake(1) }
orderSixCupcakes.setOnClickListener { orderCupcake(6) }
orderTwelveCupcakes.setOnClickListener { orderCupcake(12) }
- 在
orderCupcake()
方法中,將顯示浮動式訊息的程式碼替換成前往口味片段的程式碼。使用findNavController()
方法取得NavController
,並呼叫此方法的navigate()
,並在傳入動作 IDR.id.action_startFragment_to_flavorFragment
。請確認這項動作 ID 與您在nav_graph.xml.
中宣告的動作相符
將
fun orderCupcake(quantity: Int) {
Toast.makeText(activity, "Ordered $quantity cupcake(s)", Toast.LENGTH_SHORT).show()
}
取代為
fun orderCupcake(quantity: Int) {
findNavController().navigate(R.id.action_startFragment_to_flavorFragment)
}
- 新增「Import」
import
androidx.navigation.fragment.findNavController
,或從 Android Studio 提供的選項中選擇。
在口味和取貨片段中新增「Navigation」
這項工作與上一個工作類似,您要在其他片段 (口味和取貨片段) 中新增導覽。
- 開啟 app > java > com.example.cupcake > FlavorFragment.kt。請注意,「Next」按鈕點擊事件監聽器內呼叫的方法為
goToNextScreen()
方法。 - 在
FlavorFragment.kt
的goToNextScreen()
方法中,取代顯示浮動式訊息的程式碼,前往取貨片段。使用動作 IDR.id.action_flavorFragment_to_pickupFragment
並確認此 ID 與nav_graph.xml.
中宣告的動作相符
fun goToNextScreen() {
findNavController().navigate(R.id.action_flavorFragment_to_pickupFragment)
}
記得要 import androidx.navigation.fragment.findNavController
。
- 同樣地,在
PickupFragment.kt
的goToNextScreen()
方法中,取代現有的程式碼以前往摘要片段。
fun goToNextScreen() {
findNavController().navigate(R.id.action_pickupFragment_to_summaryFragment)
}
匯入 androidx.navigation.fragment.findNavController
。
- 執行應用程式。確認按鈕可供切換畫面瀏覽。每個片段顯示的資訊可能不完整,但請放心,您會在後續步驟中填入正確的片段。
更新應用程式列中的標題
瀏覽應用程式時,請留意應用程式列中的標題。永遠顯示為杯子蛋糕。
根據目前片段的功能提供更相關的標題,有助於改善使用者體驗。
使用 NavController
變更應用程式列 (也稱為動作列) 中各片段的標題,並使用「Up」 (←) 按鈕。
- 在
MainActivity.kt
中覆寫onCreate()
方法來設定導覽控制器。從NavHostFragment
取得NavController
的執行個體。 - 呼叫
setupActionBarWithNavController(navController)
以傳入NavController
的執行個體。操作方式如下:在應用程式列中,根據目的地標籤顯示標題;即使沒有頂層目的地,也會顯示「Up」 按鈕。
class MainActivity : AppCompatActivity(R.layout.activity_main) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController
setupActionBarWithNavController(navController)
}
}
- 在 Android Studio 顯示提示時新增必要匯入項目。
import android.os.Bundle
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupActionBarWithNavController
- 設定每個片段的應用程式列標題。開啟
navigation/nav_graph.xml
並切換至「Code」分頁標籤。 - 在
nav_graph.xml
中,修改每個片段目的地的android:label
屬性。使用已在範例應用程式中宣告的下列字串資源。
如果是起始片段,請使用 @string/app_name
和 Cupcake
值。
如果是口味片段,請使用 @string/choose_flavor
和 Choose Flavor
值。
如果是取貨片段,請使用 @string/choose_pickup_date
和 Choose Pickup Date
值。
如果是摘要片段,請使用 @string/order_summary
和 Order Summary
值。
<navigation ...>
<fragment
android:id="@+id/startFragment"
...
android:label="@string/app_name" ... >
<action ... />
</fragment>
<fragment
android:id="@+id/flavorFragment"
...
android:label="@string/choose_flavor" ... >
<action ... />
</fragment>
<fragment
android:id="@+id/pickupFragment"
...
android:label="@string/choose_pickup_date" ... >
<action ... />
</fragment>
<fragment
android:id="@+id/summaryFragment"
...
android:label="@string/order_summary" ... />
</navigation>
- 執行應用程式。請注意,在您前往各個片段目的地時,應用程式列中的標題會隨之變更。另請注意,應用程式列上會顯示「Up」按鈕 (箭頭 ←)。如果輕觸按鈕,則不會採取任何行動。在下一個程式碼研究室中,您會實作「Up」按鈕行為。
4. 建立共用 ViewModel
現在要開始在各個片段中填入正確資料。您將使用共用 ViewModel
將應用程式資料儲存在單一 ViewModel
中。應用程式中的多個片段會使用其活動範圍來存取共用的 ViewModel
。
在大部分生產環境應用程式中,不同片段間共用資料是常見的用途。舉例來說,在杯子蛋糕應用程式的 (本程式碼實驗室) 最終版本中 (請注意下方螢幕截圖),使用者在第一個畫面選取杯子蛋糕數量,系統在第二個畫面依杯子蛋糕的數量計算並顯示價格。同樣地,摘要畫面也使用口味和取貨日期等其他應用程式資料。
從應用程式功能的角度來看,您可以選擇將這筆訂單資訊儲存在單一 ViewModel
中,即可在此活動中的各片段之間共用。請記得 ViewModel
是 Android 架構元件的一部分。在設定變更期間,會保留儲存在 ViewModel
中的應用程式資料。若要在應用程式中新增 ViewModel
,您必須建立可從 ViewModel
類別擴充的新類別。
建立 OrderViewModel
在這項工作中,您會稱為 OrderViewModel
的 杯子蛋糕 應用程式建立共用的 ViewModel
。您也會將應用程式資料新增為 ViewModel
及方法中的屬性,用於更新及修改資料。以下是類別的屬性:
- 訂單數量 (
Integer
) - 杯子蛋糕口味 (
String
) - 取貨日期 (
String
) - 價格 (
Double
)
採用 ViewModel
最佳做法
在 ViewModel
,建議您勿將檢視模型資料顯示為 public
變數。否則,外部類別會以出乎意料的方式修改應用程式資料,並且建立應用程式未預期處理的各種案例。請改為建立這些可變動的屬性 private
,並導入備份屬性,並視需要公開每個屬性的 public
不可變更版本。慣例是在劃底線的 private
可變動屬性名稱 (_
) 前面加上前置碼。
請根據使用者的選擇,採用下列方式更新上述屬性:
setQuantity(numberCupcakes: Int)
setFlavor(desiredFlavor: String)
setDate(pickupDate: String)
價格不需要 setter 方法,因為系統會使用其他屬性在 OrderViewModel
內計算價格。下列步驟將說明如何導入共用 ViewModel
。
您將在專案中建立名為 model
的新套件,並新增 OrderViewModel
類別。系統會分隔瀏覽模型程式碼與其餘使用者介面程式碼 (片段和活動)。根據程式碼功能,將程式碼分隔為不同套件是程式設計的最佳做法。
- 在 Android Studio 的「Project」視窗中,以滑鼠右鍵按一下「com.example.cupcake」 >「New」 >「Package」。
- 系統會開啟「New Package」對話方塊,將套件命名為
com.example.cupcake.model
。
- 在
model
套件之下建立OrderViewModel
Kotlin 類別。在「Project」視窗中,以滑鼠右鍵按一下model
套件,然後選取「New」>「Kotlin File/Class」。在新對話方塊中,提供檔案名稱OrderViewModel
。
- 在
OrderViewModel.kt
中,變更類別簽名以從ViewModel
延伸。
import androidx.lifecycle.ViewModel
class OrderViewModel : ViewModel() {
}
- 在
OrderViewModel
類別中,將上述討論的屬性新增為private
val
。 - 請將資源型別變更為
LiveData
,然後在屬性中加入備用欄位,使得可觀測到這些屬性,而且當檢視模型中的來源資料變更時可以更新使用者介面。
private val _quantity = MutableLiveData<Int>(0)
val quantity: LiveData<Int> = _quantity
private val _flavor = MutableLiveData<String>("")
val flavor: LiveData<String> = _flavor
private val _date = MutableLiveData<String>("")
val date: LiveData<String> = _date
private val _price = MutableLiveData<Double>(0.0)
val price: LiveData<Double> = _price
您必須匯入以下類別:
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
- 在
OrderViewModel
類別中,新增上述方法。在方法中,指派傳遞至可變動屬性中的引數。 - 由於這些 setter 方法需要從檢視模型外呼叫,因此請保留為
public
方法,也就是不需要在fun
關鍵字之前使用private
或其他瀏覽權限修飾符。Kotlin 中的預設瀏覽權限修飾符設定為public
。
fun setQuantity(numberCupcakes: Int) {
_quantity.value = numberCupcakes
}
fun setFlavor(desiredFlavor: String) {
_flavor.value = desiredFlavor
}
fun setDate(pickupDate: String) {
_date.value = pickupDate
}
- 建構並執行應用程式,確保不會發生編譯錯誤。使用者介面不應出現任何可見的變更。
做得好!現在,您已經開始使用檢視模型。隨著您在應用程式中建構更多功能,就會知道您的類別需要更多屬性和方法,並在類別中加入更多。
如果您在 Android Studio 中看到以灰色字型顯示的類別名稱、屬性名稱或方法名稱,這是正常現象。這表示類別、屬性或方法或是目前未使用,但一定會存在!敬請期待。
5. 使用 ViewModel 更新使用者介面
在這項工作中,您將使用所建立的共用檢視模型來更新應用程式的使用者介面。導入共用檢視模型的主要差異,就是從使用者介面控制器存取該模型的方式。您將使用活動執行個體而非片段執行個體,後續章節將示範如何執行這項作業。
意指可在各片段之間共用檢視模型。每個片段都可以存取檢視模型,藉此查看訂單的部分詳細資訊,或是在檢視模型中更新部分資料。
更新 StartFragment 即可使用檢視模型
如要在 StartFragment
中使用共用檢視模型,您必須使用 activityViewModels()
而非 viewModels()
委派類別來初始化 OrderViewModel
。
viewModels()
提供範圍限定於目前片段的ViewModel
執行個體。不同片段各有所不同。activityViewModels()
提供範圍限定於目前活動的ViewModel
執行個體。因此,在相同活動中的多個片段中,執行個體會保持不變。
使用 Kotlin 屬性委派
在 Kotlin 中,每個可變動 (var
) 屬性都會自動產生屬性的 getter 和 setter 函式。當您指派屬性的值或讀取屬性的值時,會呼叫 setter 和 getter 函式。(針對唯讀屬性 (val
),根據預設只會產生 getter 函式。在讀取唯讀屬性的值時,會呼叫 getter 函式。)
Kotlin 中的屬性委派功能可協助您將 getter-setter 責任移交給其他類別。
此類別 (稱為「委派類別」) 可提供屬性的 getter 和 setter 函式,並處理其變更。
委派屬性是使用 by
子句和委派類別執行個體來定義:
// Syntax for property delegation
var <property-name> : <property-type> by <delegate-class>()
- 在
StartFragment
類別中,取得共用檢視模型的參照做為類別變數。使用fragment-ktx
程式庫中的by activityViewModels()
Kotlin 屬性委派。
private val sharedViewModel: OrderViewModel by activityViewModels()
您可能需要匯入以下新資料:
import androidx.fragment.app.activityViewModels
import com.example.cupcake.model.OrderViewModel
- 針對
FlavorFragment
、PickupFragment
、SummaryFragment
類別重複上述步驟,您將在程式碼研究室的後續章節中使用此sharedViewModel
執行個體。 - 返回
StartFragment
類別後,即可使用檢視模型。在orderCupcake()
方法的開頭,先在共用檢視模型中呼叫setQuantity()
方法來更新數量,之後再前往口味片段。
fun orderCupcake(quantity: Int) {
sharedViewModel.setQuantity(quantity)
findNavController().navigate(R.id.action_startFragment_to_flavorFragment)
}
- 在
OrderViewModel
類別中新增下列方法以檢查是否已設定訂單的口味。您將在後續步驟中在StartFragment
類別中使用此方法。
fun hasNoFlavorSet(): Boolean {
return _flavor.value.isNullOrEmpty()
}
- 在
StartFragment
類別中的orderCupcake()
方法中,在設定數量後,若未設定口味,則先將預設口味設定為「Vanilla」(香草),之後再前往口味片段。完整的方法看起來會像這樣:
fun orderCupcake(quantity: Int) {
sharedViewModel.setQuantity(quantity)
if (sharedViewModel.hasNoFlavorSet()) {
sharedViewModel.setFlavor(getString(R.string.vanilla))
}
findNavController().navigate(R.id.action_startFragment_to_flavorFragment)
}
- 建構應用程式以確保不會發生編譯錯誤。但使用者介面並不會出現任何可見的變更。
6. 搭配使用 ViewModel 與資料繫結
接下來,您需要使用資料繫結將檢視模型資料繫結至使用者介面。系統也會根據使用者在使用者介面中的選擇,更新共用檢視模型。
複習資料繫結
請注意,資料繫結程式庫 是 Android Jetpack 的一部分。資料繫結使用宣告式格式,將版面配置中的使用者介面元件繫結至應用程式中的資料來源。簡單來說,資料繫結將資料 (從程式碼) 繫結至檢視 + 檢視表繫結 (將檢視繫結至程式碼)。設定這些繫結,並啟用自動進行更新後,即使您忘記從程式碼手動更新使用者介面,也能降低錯誤發生的機率。
用使用者的選擇來更新品味
- 在
layout/fragment_flavor.xml
中,在根<layout>
標記內新增<data>
標記。新增名為viewModel
且型別為com.example.cupcake.model.OrderViewModel
的版面配置變數。請確認型別屬性中的套件名稱與應用程式內共用檢視模型類別OrderViewModel
的套件名稱相符。
<layout ...>
<data>
<variable
name="viewModel"
type="com.example.cupcake.model.OrderViewModel" />
</data>
<ScrollView ...>
...
- 同樣地,請針對
fragment_pickup.xml
及fragment_summary.xml
重複上率步驟以新增viewModel
版面配置變數。您將在接下來幾節中使用此變數。此版面配置未使用共用檢視模型,因此您不需要在fragment_start.xml
中加入這段程式碼。 - 在
FlavorFragment
類別的onViewCreated()
中,將檢視模型執行個體與版面配置模型中的共用檢視模型執行個體繫結。在binding?.
apply
區塊中加入以下程式碼。
binding?.apply {
viewModel = sharedViewModel
...
}
套用範圍函式
這可能是您首次在 Kotlin 中看到 apply
函式。apply
是 Kotlin 標準程式庫中的 範圍函式。會在物件結構定義內執行程式碼區塊。這樣可以建立臨時範圍,而且您可以在該範圍中即可存取物件而無需物件名稱。apply
的常見用途是設定物件。這類呼叫可以解讀為「套用下列指派作業至物件」。
範例:
clark.apply {
firstName = "Clark"
lastName = "James"
age = 18
}
// The equivalent code without apply scope function would look like the following.
clark.firstName = "Clark"
clark.lastName = "James"
clark.age = 18
- 在
PickupFragment
和SummaryFragment
類別中,針對onViewCreated()
方法重複上述步驟。
binding?.apply {
viewModel = sharedViewModel
...
}
- 在
fragment_flavor.xml
中,使用新的版面配置變數viewModel
,根據檢視模型的flavor
值,設定圓形按鈕的checked
屬性。如果圓形按鈕表示的口味與儲存在檢視模型中的口味相同,則圓形按鈕顯示為已選取 (checked
= true)。已選取「Vanilla」(香草)RadioButton
的狀態繫結運算式如下所示:
@{viewModel.flavor.equals(@string/vanilla)}
基本上,您會使用 equals
函式來比較 viewModel.flavor
屬性與對應的字串資源,藉此判定已檢查狀態是否為 True 或 False。
<RadioGroup
...>
<RadioButton
android:id="@+id/vanilla"
...
android:checked="@{viewModel.flavor.equals(@string/vanilla)}"
.../>
<RadioButton
android:id="@+id/chocolate"
...
android:checked="@{viewModel.flavor.equals(@string/chocolate)}"
.../>
<RadioButton
android:id="@+id/red_velvet"
...
android:checked="@{viewModel.flavor.equals(@string/red_velvet)}"
.../>
<RadioButton
android:id="@+id/salted_caramel"
...
android:checked="@{viewModel.flavor.equals(@string/salted_caramel)}"
.../>
<RadioButton
android:id="@+id/coffee"
...
android:checked="@{viewModel.flavor.equals(@string/coffee)}"
.../>
</RadioGroup>
事件監聽器繫結
事件監聽器繫結是指在事件發生 (例如 onClick
事件) 時執行的 lambda 運算式。做法類似於方法參照 (例如 textview.setOnClickListener(clickListener)
),但事件監聽器繫結可讓您執行任意資料繫結運算式。
- 在
fragment_flavor.xml
中,使用監聽器繫結將事件監聽器新增至圓形按鈕。使用不含參數的 lambda 運算式,並呼叫viewModel
。setFlavor()
方法藉由傳入對應的口味字串資源。
<RadioGroup
...>
<RadioButton
android:id="@+id/vanilla"
...
android:onClick="@{() -> viewModel.setFlavor(@string/vanilla)}"
.../>
<RadioButton
android:id="@+id/chocolate"
...
android:onClick="@{() -> viewModel.setFlavor(@string/chocolate)}"
.../>
<RadioButton
android:id="@+id/red_velvet"
...
android:onClick="@{() -> viewModel.setFlavor(@string/red_velvet)}"
.../>
<RadioButton
android:id="@+id/salted_caramel"
...
android:onClick="@{() -> viewModel.setFlavor(@string/salted_caramel)}"
.../>
<RadioButton
android:id="@+id/coffee"
...
android:onClick="@{() -> viewModel.setFlavor(@string/coffee)}"
.../>
</RadioGroup>
- 執行應用程式,並注意在口味片段中如何依據預設選取的「Vanilla」(香草) 選項。
漂亮!現在可以移到下一個片段。
7. 更新取貨和摘要片段以使用檢視模型
瀏覽應用程式,並留意,在取貨片段中的圓形按鈕選項標籤為空白。在這項工作中,會計算 4 個可用的取貨日期,並顯示在取貨片段中。很多種顯示格式化日期的方法有,Android 提供多種實用的公用程式來執行此動作。
建立取貨選項清單
日期格式轉換程式
Android 架構提供一個稱為 SimpleDateFormat
的類別,該類別會以區分地區設定方式來格式化並剖析日期。這允許進行日期格式化 (日期 → 文字) 及剖析 (文字 → 日期)。
您可以傳入模式字串和地區設定,以建立 SimpleDateFormat
的執行個體:
SimpleDateFormat("E MMM d", Locale.getDefault())
"E MMM d"
等格式字串表示日期和時間格式。從 'A'
到 'Z'
和 'a'
到 'z'
的字母都會視為模式字母,代表日期或時間字串的元件。例如,d
代表一個月中的日期、y
代表年份,M
代表月份。如果日期是 2018 年 1 月 4 日,則模式字串 "EEE, MMM d"
會剖析為 "Wed, Jul 4"
。若需完整的模型字母清單,請參閱 說明文件。
Locale
物件代表特定的地理區域、政治或文化區域。代表語言/國家/地區/變化版本組合。地區設定會根據當地慣例來改變資訊 (例如數字或日期) 的顯示方式。由於世界不同地區表達日期和時間的格式不同,因此日期和時間會依地區設定而有所不同。您將使用 Locale.getDefault()
方法擷取使用者裝置上設定的地區設定資訊,並傳遞至 SimpleDateFormat
建構函式中。
Android 中的地區設定是語言和國家/地區代碼的組合。語言代碼是兩個小寫英文字母 ISO 語言代碼,例如「en」表示英文。國家/地區代碼是由兩個大寫英文字母組成的 ISO 國家/地區代碼,例如美國為「US」。
現在使用 SimpleDateFormat
和 Locale
來判斷 杯子蛋糕 應用程式的可用取貨日期。
- 在
OrderViewModel
類別中新增名為getPickupOptions()
的函式,以便建立並傳回取貨日期清單。在方法中,建立一個名為options
的val
變數,然後初始化為mutableListOf
<String>()
。
private fun getPickupOptions(): List<String> {
val options = mutableListOf<String>()
}
- 使用
SimpleDateFormat
傳遞模式字串"E MMM d"
和地區設定來建立格式轉換程式字串。在模式字串中,E
代表星期幾,且會剖析為「Tue Dec 10」(12 月 10 日星期四)。
val formatter = SimpleDateFormat("E MMM d", Locale.getDefault())
當 Android Studio 出現提示時,匯入 java.text.SimpleDateFormat
和 java.util.Locale
。
- 取得
Calendar
執行個體並指派給新的變數,將其設定為val
。此變數會包含目前的日期和時間。而且匯入java.util.Calendar
。
val calendar = Calendar.getInstance()
- 建置當天日期加上後續三個日期的日期清單。由於這需要 4 個日期選項,請重複此程式碼區塊 4 次。此
repeat
區塊會格式化日期,將其加到日期選項清單中,然後將日曆增加 1 天。
repeat(4) {
options.add(formatter.format(calendar.time))
calendar.add(Calendar.DATE, 1)
}
- 在方法的結束時傳回更新後的
options
。以下是已完成的方法:
private fun getPickupOptions(): List<String> {
val options = mutableListOf<String>()
val formatter = SimpleDateFormat("E MMM d", Locale.getDefault())
val calendar = Calendar.getInstance()
// Create a list of dates starting with the current date and the following 3 dates
repeat(4) {
options.add(formatter.format(calendar.time))
calendar.add(Calendar.DATE, 1)
}
return options
}
- 在
OrderViewModel
類別中,新增名為val
的類別屬性dateOptions
。使用您剛剛建立的getPickupOptions()
方法進行初始化。
val dateOptions = getPickupOptions()
更新版面配置以顯示取貨選項
現在檢視模型中有四個可用的取貨日期,更新 fragment_pickup.xml
版面配置以顯示這些日期。您還可以使用資料繫結來顯示每個圓形按鈕的已勾選狀態,並在選取不同圓形按鈕時更新檢視模型的日期。實作方式類似於口味片段中的資料繫結。
在 fragment_pickup.xml
中:
在 viewModel
中,圓形按鈕 option0
代表 dateOptions[0]
(今天)
在 viewModel
中,圓形按鈕 option1
代表 dateOptions[1]
(明天)
在 viewModel
中,圓形按鈕 option2
代表 dateOptions[2]
(後天)
在 viewModel
中,圓形按鈕 option3
代表 dateOptions[3]
(大後天)
- 在
fragment_pickup.xml
中,針對option0
圓形按鈕使用新的版面配置變數viewModel
,以便根據檢視模型中的date
值來設定checked
屬性。比較viewModel.date
屬性與dateOptions
清單中的第一個字串 (也就是當天日期)。使用equals
函式進行比較,最終繫結運算式如下所示:
@{viewModel.date.equals(viewModel.dateOptions[0])}
- 針對相同的圓形按鈕,使用事件監聽器繫結新增事件監聽器至
onClick
屬性。按一下此按鈕後,viewModel
就會呼叫setDate()
,傳入dateOptions[0]
。 - 如果是相同的圓形按鈕,請將
text
屬性值設定為dateOptions
清單中的第一個字串。
<RadioButton
android:id="@+id/option0"
...
android:checked="@{viewModel.date.equals(viewModel.dateOptions[0])}"
android:onClick="@{() -> viewModel.setDate(viewModel.dateOptions[0])}"
android:text="@{viewModel.dateOptions[0]}"
...
/>
- 針對其他圓形按鈕重複上述步驟,並據此變更
dateOptions
的索引。
<RadioButton
android:id="@+id/option1"
...
android:checked="@{viewModel.date.equals(viewModel.dateOptions[1])}"
android:onClick="@{() -> viewModel.setDate(viewModel.dateOptions[1])}"
android:text="@{viewModel.dateOptions[1]}"
... />
<RadioButton
android:id="@+id/option2"
...
android:checked="@{viewModel.date.equals(viewModel.dateOptions[2])}"
android:onClick="@{() -> viewModel.setDate(viewModel.dateOptions[2])}"
android:text="@{viewModel.dateOptions[2]}"
... />
<RadioButton
android:id="@+id/option3"
...
android:checked="@{viewModel.date.equals(viewModel.dateOptions[3])}"
android:onClick="@{() -> viewModel.setDate(viewModel.dateOptions[3])}"
android:text="@{viewModel.dateOptions[3]}"
... />
- 執行應用程式後,當取貨選項可用時就會看到未來幾天。螢幕截圖取決於您的目前日期而有所不同。請注意,依據預設不會選取任何選項。您將在下一個步驟中導入此動作。
- 在
OrderViewModel
類別中,建立名為resetOrder()
的函式,重設檢視模型中的MutableLiveData
屬性。將dateOptions
清單中的目前日期值指派給_date.
value.
fun resetOrder() {
_quantity.value = 0
_flavor.value = ""
_date.value = dateOptions[0]
_price.value = 0.0
}
- 在類別中新增
init
區塊,並從該類別呼叫新方法resetOrder()
。
init {
resetOrder()
}
- 從類別中的屬性宣告移除初始值。您目前正在使用
init
區塊來在建立OrderViewModel
執行個體時初始化屬性。
private val _quantity = MutableLiveData<Int>()
val quantity: LiveData<Int> = _quantity
private val _flavor = MutableLiveData<String>()
val flavor: LiveData<String> = _flavor
private val _date = MutableLiveData<String>()
val date: LiveData<String> = _date
private val _price = MutableLiveData<Double>()
val price: LiveData<Double> = _price
- 再次執行應用程式。請注意,依據預設會選取今天的日期。
更新摘要片段以使用檢視模型
現在介紹最後一個片段。訂單摘要片段是用來顯示訂單詳細資料的摘要。在這項工作中,請妥善利用共用檢視模型中的所有訂單資訊,並使用資料繫結來更新螢幕上的訂單詳細資料。
- 請務必在
fragment_summary.xml
中宣告資料檢視模型資料變數viewModel
。
<layout ...>
<data>
<variable
name="viewModel"
type="com.example.cupcake.model.OrderViewModel" />
</data>
<ScrollView ...>
...
- 在
SummaryFragment
中,在onViewCreated()
中確認binding.viewModel
已初始化。 - 在
fragment_summary.xml
中,從資料檢視模型中讀取,以更新畫面中的訂單摘要詳細資料。新增下列文字屬性來更新數量、口味和日期TextViews
。數量為Int
型別,因此您必須將其轉換為字串。
<TextView
android:id="@+id/quantity"
...
android:text="@{viewModel.quantity.toString()}"
... />
<TextView
android:id="@+id/flavor"
...
android:text="@{viewModel.flavor}"
... />
<TextView
android:id="@+id/date"
...
android:text="@{viewModel.date}"
... />
- 執行並測試應用程式,確認您所選的訂單選項已顯示在訂單摘要中。
8. 依據訂單詳細資料計算價格
查看本程式碼研究室的最終應用程式螢幕截圖時,您會注意到價格確實顯示在每個片段 (StartFragment
除外) 上,因此使用者在建立訂單時即可得知價格。
以下是我們的杯子蛋糕專賣店如何計算價格的規則。
- 每個杯子蛋糕 $2.00 美元
- 對於當天取貨,訂單中會再增加 $3.00 美元
因此,6 個杯子蛋糕訂單的價格為 6 個杯子蛋糕 x 每個 $2 美元 = $12 美元。如果同一位使用者想要當天取貨,$3 美元的附加費用會讓訂單總價變成 $15 美元。
更新檢視模型中的價格
若要在應用程式中支援這項功能,請先處理杯子蛋糕的價格,並立即忽略當天取貨費用。
- 開啟
OrderViewModel.kt
,然後將每杯子蛋糕的價格儲存在變數中。在類別定義之外 (但在匯入陳述式之後),將此變數宣告為檔案頂端的頂層私人常數。使用const
修飾符,並使用val
設定為唯讀。
package ...
import ...
private const val PRICE_PER_CUPCAKE = 2.00
class OrderViewModel : ViewModel() {
...
請記得,常數值 (在 Kotlin 中以 const
關鍵字標記) 不會變更,而且會在編譯期間知道該值。若要進一步瞭解常數,請參閱 說明文件。
- 您已經定義了每杯子蛋糕的價格,請建立計算價格的輔助方法。此方法可能是
private
,因為只能在此類別中使用。您會在下一個工作中變更價格邏輯,以便納入當天取貨費用。
private fun updatePrice() {
_price.value = (quantity.value ?: 0) * PRICE_PER_CUPCAKE
}
這段程式碼會將每杯子蛋糕的價格乘以所訂購的杯子蛋糕數量。關於括號中的程式碼,因為 quantity.value
的值可以是空值,所以使用 elvis 運算子 (?:
)。elvis 運算子 (?:
) 表示左側運算式不是空值時,則使用該運算子。如果左側運算式是空值,則使用 elvis 運算子右側的運算式 (本例中為 0
)。
- 在相同
OrderViewModel
類別中,在設定數量時更新價格變數。在setQuantity()
函式中呼叫新函式。
fun setQuantity(numberCupcakes: Int) {
_quantity.value = numberCupcakes
updatePrice()
}
將價格屬性繫結到使用者介面
- 在
fragment_flavor.xml
、fragment_pickup.xml
和fragment_summary.xml
的版面配置中,確認已定義com.example.cupcake.model.OrderViewModel
型別的資料變數viewModel
。
<layout ...>
<data>
<variable
name="viewModel"
type="com.example.cupcake.model.OrderViewModel" />
</data>
<ScrollView ...>
...
- 在每個片段類別的
onViewCreated()
方法中,請務必將片段中的檢視模型物件執行個體繫結至版面配置中的檢視模型資料變數。
binding?.apply {
viewModel = sharedViewModel
...
}
- 在每個片段版面配置中,如果在版面配置中有顯示價格,則使用
viewModel
變數來設定價格。以修改fragment_flavor.xml
檔案開始。舉例而言,如果是subtotal
文字檢視區塊,請將android:text
屬性的值設定為"@{@string/subtotal_price(viewModel.price)}".
。此資料繫結版面配置運算式使用字串資源@string/subtotal_price
,並傳入參數,亦即來自檢視模型的價格,因此輸出會顯示「Subtotal 12.0」(小計 12.0)。
...
<TextView
android:id="@+id/subtotal"
android:text="@{@string/subtotal_price(viewModel.price)}"
... />
...
您正在使用 strings.xml
檔案所宣告的此字串資源:
<string name="subtotal_price">Subtotal %s</string>
- 執行應用程式。如果您在起始片段中選取「One cupcake」(一個杯子蛋糕),口味片段會顯示「Subtotal 2.0」(小計 2.0)。如果您選取「Six cupcake」(六個杯子蛋糕),口味片段會顯示「Subtotal 12.0」(小計 12.0),以此類推。您稍後會將價格格式化為適當的貨幣格式,因此此行為目前是正常現象。
- 現在針對取貨和摘要片段進行類似的變更。在
fragment_pickup.xml
和fragment_summary.xml
版面配置中,也修改文字檢視區塊以使用viewModel
price
屬性。
fragment_pickup.xml
...
<TextView
android:id="@+id/subtotal"
...
android:text="@{@string/subtotal_price(viewModel.price)}"
... />
...
fragment_summary.xml
…
<TextView
android:id="@+id/total"
...
android:text="@{@string/total_price(viewModel.price)}"
... />
…
- 執行應用程式。確認訂單摘要中顯示的價格是按訂單量 1、6 和 12 個杯子蛋糕正確計算所得。如前所述,預期此刻價格格式不正確 ($2 會顯示為 2.0,$12 會顯示為 12.0)。
當天取貨附加費
在這項工作中,您將導入第二項規則,就是當天取貨會額外收取 $3.00 美元。
- 在
OrderViewModel
類別中,針對當天取貨費用定義新頂層私人常數。
private const val PRICE_FOR_SAME_DAY_PICKUP = 3.00
- 在
updatePrice()
中,檢查使用者是否選取了當天取貨。檢查檢視模型 (_date.
value
) 中的日期是否與dateOptions
清單中的第一個項目相同 (始終是當天)。
private fun updatePrice() {
_price.value = (quantity.value ?: 0) * PRICE_PER_CUPCAKE
if (dateOptions[0] == _date.value) {
}
}
- 為了簡化這些計算,請引入暫時變數
calculatedPrice
。計算更新的價格,然後將其重新指派給_price.
value
。
private fun updatePrice() {
var calculatedPrice = (quantity.value ?: 0) * PRICE_PER_CUPCAKE
// If the user selected the first option (today) for pickup, add the surcharge
if (dateOptions[0] == _date.value) {
calculatedPrice += PRICE_FOR_SAME_DAY_PICKUP
}
_price.value = calculatedPrice
}
- 從
setDate()
方法呼叫updatePrice()
輔助方法,即可指定當天取貨費用。
fun setDate(pickupDate: String) {
_date.value = pickupDate
updatePrice()
}
- 執行應用程式,瀏覽應用程式。您會發現,變更取貨日期不會從總價中扣除當天取貨費用。這是因為在檢視模型中價格有所變更,但不會通知繫結版面配置。
設定生命週期擁有者以觀測 LiveData
LifecycleOwner
是一個具有 Android 生命週期的類別,例如活動或片段。唯有生命週期擁有者處於有效狀態 (STARTED
或 RESUMED
),LiveData
觀測程式才會觀測應用程式資料的變更。
在應用程式中,LiveData
物件或可觀測資料是檢視模型中的 price
屬性。生命週期擁有者是口味、取貨和摘要片段。LiveData
觀測程式是版面配置檔案中的繫結運算式,當中包含可觀測資料,例如價格。透過資料繫結,可觀測值變更時,也會自動更新其繫結的使用者介面元素。
繫結運算式範例:android:text="@{@string/subtotal_price(viewModel.price)}"
為了自動更新使用者介面元素,您必須在應用程式中建立 binding.
lifecycleOwner
與生命週期擁有者的關聯。接下來,您將導入這項動作。
- 在
FlavorFragment
、PickupFragment
、SummaryFragment
類別的onViewCreated()
方法中,在binding?.apply
區塊中加入下列內容。這項操作會設定繫結物件的生命週期擁有者。設定生命週期擁有者,應用程式將能觀測LiveData
物件。
binding?.apply {
lifecycleOwner = viewLifecycleOwner
...
}
- 再次執行應用程式。在取貨畫面中,變更取貨日期,並留意自動變更價格方式的差異。且在摘要畫面中正確反映取貨費用。
- 請注意,當您選取當天取貨時,訂單價格會增加 $3.00 美元。選取未來日期的價格應是杯子蛋糕數量 x $2.00 美元。
- 使用不同杯子蛋糕數量、口味和取貨日期來測試不同情況。現在,您應該會在每個片段上看到檢視模型的更新價格。最棒的是,您不用撰寫額外的 Kotlin 程式碼,就能讓使用者介面每次都更新價格。
如要完成價格功能的導入,您必須將價格格式設定為當地幣別。
使用 LiveData 轉換來轉換價格的格式
LiveData
transformation 方法可讓您針對來源 LiveData
執行資料操縱,並傳回產生的 LiveData
物件。簡單來說,此會將 LiveData
的值轉換為其他值。除非觀測程式觀測到 LiveData
物件,否則不會計算這些轉換。
Transformations.map()
其中一種是轉換函式,此方法採用 LiveData
和函式做為參數。函式會操控來源 LiveData
,並傳回可觀測的更新值。
以下列舉幾個可使用 LiveData 轉換的範例:
- 設定顯示的日期、時間字串等格式
- 項目清單排序
- 篩選項目或將項目分組
- 從清單計算結果,例如所有項目的加總、項目數目、傳回的最後一個項目等等。
在這項工作中,您必須使用 Transformations.map()
方法將設定價格的格式為使用當地幣別。這會將原始價格從十進位值 (LiveData<Double>
) 轉換為字串值 (LiveData<String>
)。
- 在
OrderViewModel
類別中,將幕後屬性類型變更為LiveData<String>
,而不是LiveData<Double>.
。價格的格式將為包含貨幣符號 (例如「$」) 的字串。您將在下一步驟修正初始化錯誤。
private val _price = MutableLiveData<Double>()
val price: LiveData<String>
- 使用
Transformations.map()
初始化新變數,並傳入_price
和 lambda 函式。請在NumberFormat
類別中使用getCurrencyInstance()
方法,以將價格轉換成當地幣別格式。轉換程式碼看起來會像這樣。
private val _price = MutableLiveData<Double>()
val price: LiveData<String> = Transformations.map(_price) {
NumberFormat.getCurrencyInstance().format(it)
}
請匯入androidx.lifecycle.Transformations
和 java.text.NumberFormat
。
- 執行應用程式。現在畫面上應會顯示小計及總計的字串格式價格。更容易使用!
- 測試是否正常運作。測試範例:訂購一個杯子蛋糕、訂購六個杯子蛋糕、訂購 12 個杯子蛋糕。確認每個螢幕上的價格正確無誤。口味和取貨片段中應顯示「Subtotal $2.00」,而訂單摘要中應顯示「Total $2.00」。也請確認訂單摘要顯示正確的訂單詳細資料。
9. 使用事件監聽器繫結設定點擊事件監聽器
在這項工作中,您將使用事件監聽器繫結來將片段類別中的按鈕點擊事件監聽器繫結至版面配置。
- 在版面配置檔案
fragment_start.xml
中,新增名為startFragment
且型別為com.example.cupcake.StartFragment
的資料變數。確認片段的套件名稱與應用程式的套件名稱相符。
<layout ...>
<data>
<variable
name="startFragment"
type="com.example.cupcake.StartFragment" />
</data>
...
<ScrollView ...>
- 在
StartFragment.kt
的onViewCreated()
方法中,將新的資料變數繫結至片段執行個體。您可以使用this
關鍵字來存取片段中的片段執行個體。移除binding?.
apply
區塊以及區塊內的程式碼。已完成的方法應如下所示。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding?.startFragment = this
}
- 在
fragment_start.xml
中,新增使用事件監聽器繫結的事件監聽器至按鈕的onClick
屬性,在startFragment
上呼叫orderCupcake()
,並傳遞杯子蛋糕的數量。
<Button
android:id="@+id/order_one_cupcake"
android:onClick="@{() -> startFragment.orderCupcake(1)}"
... />
<Button
android:id="@+id/order_six_cupcakes"
android:onClick="@{() -> startFragment.orderCupcake(6)}"
... />
<Button
android:id="@+id/order_twelve_cupcakes"
android:onClick="@{() -> startFragment.orderCupcake(12)}"
... />
- 執行應用程式。請注意,起始片段中的按鈕點擊處理常式運作正常。
- 同樣地,您也可以在其他版面配置中加入上述資料變數,以便繫結片段執行個體
fragment_flavor.xml
、fragment_pickup.xml
和fragment_summary.xml
。
在 fragment_flavor.xml
中
<layout ...>
<data>
<variable
... />
<variable
name="flavorFragment"
type="com.example.cupcake.FlavorFragment" />
</data>
<ScrollView ...>
在 fragment_pickup.xml
中:
<layout ...>
<data>
<variable
... />
<variable
name="pickupFragment"
type="com.example.cupcake.PickupFragment" />
</data>
<ScrollView ...>
在 fragment_summary.xml
中:
<layout ...>
<data>
<variable
... />
<variable
name="summaryFragment"
type="com.example.cupcake.SummaryFragment" />
</data>
<ScrollView ...>
- 在其餘片段類別中的
onViewCreated()
中,刪除可手動設定按鈕上點擊事件監聽器的程式碼。 - 在
onViewCreated()
方法中,會繫結片段資料變數與片段執行個體。您必須在這裡以不同方式使用this
關鍵字,因為在binding?.apply
區塊中,關鍵字this
是指繫結執行個體,而不是片段執行個體。使用@
並明確指定片段類別名稱,例如this@FlavorFragment
。已完成的onViewCreated()
方法如下所示:
FlavorFragment
類別中的 onViewCreated()
方法應如下所示:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding?.apply {
lifecycleOwner = viewLifecycleOwner
viewModel = sharedViewModel
flavorFragment = this@FlavorFragment
}
}
PickupFragment
類別中的 onViewCreated()
方法應如下所示:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding?.apply {
lifecycleOwner = viewLifecycleOwner
viewModel = sharedViewModel
pickupFragment = this@PickupFragment
}
}
在 SummaryFragment
類別方法中產生的 onViewCreated()
方法應如下所示:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding?.apply {
lifecycleOwner = viewLifecycleOwner
viewModel = sharedViewModel
summaryFragment = this@SummaryFragment
}
}
- 同樣地,在其他版面配置檔案中,新增事件監聽器繫結運算式至按鈕的
onClick
屬性。
在 fragment_flavor.xml
中:
<Button
android:id="@+id/next_button"
android:onClick="@{() -> flavorFragment.goToNextScreen()}"
... />
在 fragment_pickup.xml
中:
<Button
android:id="@+id/next_button"
android:onClick="@{() -> pickupFragment.goToNextScreen()}"
... />
在 fragment_summary.xml
中:
<Button
android:id="@+id/send_button"
android:onClick="@{() -> summaryFragment.sendOrder()}"
...>
- 執行應用程式以驗證按鈕是否正常運作。您應該不會發現行為變更,但已使用事件監聽器繫結設定點擊事件監聽器!
恭喜您完成程式碼研究室,打造 杯子蛋糕 應用程式!但是應用程式尚未處理完畢。在接下來的程式碼研究室中,您將新增「Cancel」按鈕並修改返回堆疊。您也會瞭解什麼是返回堆疊和其他新主題。到時見!
10. 解決方案程式碼
本程式碼研究室的解決方案程式碼位於下方顯示的專案中。使用 viewmodel 分支版本提取或下載程式碼。
如要取得本程式碼研究室的程式碼,並在 Android Studio 中開啟,請按照下列步驟操作。
取得程式碼
- 按一下上面顯示的網址。系統會在瀏覽器中開啟專案的 GitHub 頁面。
- 在專案的 GitHub 頁面中,按一下「Code」按鈕開啟對話方塊。
- 在對話方塊中,按一下「Download ZIP」按鈕,將專案儲存到電腦。等待下載作業完成。
- 在電腦中找到該檔案 (可能位於「下載」資料夾中)。
- 按兩下解壓縮 ZIP 檔案。這項操作會建立含有專案檔案的新資料夾。
在 Android Studio 中開啟專案
- 啟動 Android Studio。
- 在「Welcome to Android Studio」視窗中,按一下「Open an existing Android Studio project」。
注意:如果 Android Studio 已開啟,請依序選取「File」>「New」>「Import Project」選單選項。
- 在「Import Project」對話方塊中,前往解壓縮專案資料夾所在的位置 (可能位於「下載」資料夾中)。
- 按兩下該專案資料夾。
- 等待 Android Studio 開啟專案。
- 按一下「Run」按鈕 即可建構並執行應用程式。請確認應用程式的建構符合預期。
- 在「Project」工具視窗中瀏覽專案檔案,查看應用程式的設定方式。
11. 摘要
ViewModel
是 Android 架構元件 的一部分,在設定變更期間會保留儲存在ViewModel
中的應用程式資料。若要在應用程式中加入ViewModel
,請建立新類別,並從ViewModel
類別擴充該類別。- 共用
ViewModel
會將應用程式的資料從多個片段儲存在單一ViewModel
中 應用程式中的多個片段會使用其活動範圍來存取共用的ViewModel
。 LifecycleOwner
是一個具有 Android 生命週期的類別,例如活動或片段。- 唯有生命週期擁有者處於有效狀態 (
STARTED
或RESUMED
),LiveData
觀測程式才會觀測應用程式資料的變更。 - 事件監聽器繫結是指在事件發生時 (例如
onClick
事件) 執行的 lambda 運算式。做法類似於方法參照 (例如textview.setOnClickListener(clickListener)
),但事件監聽器繫結可讓您執行任意資料繫結運算式。 LiveData
transformation 方法可讓您針對來源LiveData
執行資料操縱,並傳回產生的LiveData
物件。- Android 架構提供了一個名為
SimpleDateFormat
的類別,該類別會以區分地區設定方式來格式化並剖析日期。這允許進行日期格式化 (日期 → 文字) 及剖析 (文字 → 日期)。