在各片段之間共用 ViewModel

1. 事前準備

您已學會如何使用活動、片段、意圖、資料繫結、導覽元件,也瞭解架構元件的基本概念。在本程式碼研究室中,您必須集中統整所有工作,並處理杯子蛋糕訂購應用程式,這是一項進階範例。

您將瞭解如何使用共用 ViewModel 在相同活動的片段之間共用資料,還有 LiveData 轉換等新概念。

必要條件

  • 能讀懂且理解 XML 格式的 Android 版面配置
  • 熟悉 Jetpack Navigation 元件的基本概念
  • 能在應用程式中建立含有片段目的地的導覽圖
  • 曾在活動中使用片段
  • 知道如何建立 ViewModel 來儲存應用程式資料
  • 會搭配使用 LiveData 與資料繫結,以 ViewModel 中的應用程式資料隨時更新 UI

課程內容

  • 如何在更進階用途中導入建議的應用程式架構做法
  • 如何在活動的各個片段中使用共用的 ViewModel
  • 如何套用 LiveData 轉換

建構項目

  • 杯子蛋糕應用程式會顯示杯子蛋糕的訂單流程,讓使用者選擇杯子蛋糕口味、數量和取貨日期。

需求條件

  • 已安裝 Android Studio 的電腦。
  • 杯子蛋糕應用程式的範例程式碼

2. 範例應用程式總覽

杯子蛋糕應用程式總覽

杯子蛋糕應用程式示範如何設計及導入線上訂購應用程式。本課程結束時,您將完成有下列畫面的杯子蛋糕應用程式。使用者可以選擇杯子蛋糕訂單的數量、口味和其他選項。

732881cfc463695d.png

下載本程式碼研究室的範例程式碼

本程式碼研究室提供範例程式碼,讓您以本程式碼研究室所教授的功能擴充應用程式。範例程式碼含有您先前在程式碼研究室中熟悉的程式碼。

請注意,如果您從 GitHub 下載範例程式碼,專案的資料夾名稱會是 android-basics-kotlin-cupcake-app-starter。在 Android Studio 中開啟專案時,請選取此資料夾。

如要取得本程式碼研究室的程式碼,並在 Android Studio 中開啟,請按照下列步驟操作。

取得程式碼

  1. 按一下上面顯示的網址。系統會在瀏覽器中開啟專案的 GitHub 頁面。
  2. 在專案的 GitHub 頁面中,按一下「Code」按鈕開啟對話方塊。

5b0a76c50478a73f.png

  1. 在對話方塊中,按一下「Download ZIP」按鈕,將專案儲存到電腦。等待下載作業完成。
  2. 在電腦中找到該檔案 (可能位於「下載」資料夾中)。
  3. 按兩下解壓縮 ZIP 檔案。這項操作會建立含有專案檔案的新資料夾。

在 Android Studio 中開啟專案

  1. 啟動 Android Studio。
  2. 在「Welcome to Android Studio」視窗中,按一下「Open an existing Android Studio project」

36cc44fcf0f89a1d.png

注意:如果 Android Studio 已開啟,請依序選取「File」>「New」>「Import Project」選單選項。

21f3eec988dcfbe9.png

  1. 在「Import Project」對話方塊中,前往解壓縮專案資料夾所在的位置 (可能位於「下載」資料夾中)。
  2. 按兩下該專案資料夾。
  3. 等待 Android Studio 開啟專案。
  4. 按一下「Run」按鈕 11c34fc5e516fb1c.png 即可建構並執行應用程式。請確認應用程式的建構符合預期。
  5. 在「Project」工具視窗中瀏覽專案檔案,查看應用程式的設定方式。

範例程式碼逐步操作說明

  1. 在 Android Studio 中開啟已下載的專案。專案的資料夾名稱為 android-basics-kotlin-cupcake-app-starter。然後執行應用程式。
  2. 瀏覽檔案以瞭解範例程式碼。針對版面配置檔案,您可以使用右上角的「Split」選項,同時查看版面配置和 XML 的預覽畫面。
  3. 編譯並執行應用程式時,您會發現應用程式並不完整。除了顯示 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.ktPickupFragment.ktSummaryFragment.kt 類別主要包含樣板程式碼,以及「Next」或「Send Order to Another App」按鈕的按鈕點擊處理常式,且會顯示浮動式訊息。

資源 (res 資料夾):

  • drawable 資料夾包含第一個畫面的杯子蛋糕素材資源,以及啟動器圖示檔案。
  • navigation/nav_graph.xml 包含四個片段目的地 (startFragmentflavorFragmentpickupFragmentsummaryFragment),但不含「動作」,您稍後才會在程式碼研究室中加以定義。
  • values 資料夾包含顏色、維度、字串、樣式和用於自訂應用程式佈景主題的佈景主題。建議您從先前的程式碼研究室熟悉相關資源型別。

3. 完成導覽圖

在這項工作中,我們將連結杯子蛋糕應用程式的畫面,並在應用程式中導入適當的導覽功能。

您記得必須使用導覽元件嗎?請按照這份指南中的複習,瞭解如何設定專案和應用程式,目標是:

在導覽圖中連接目的地

  1. 在 Android Studio 的「Project」視窗中,開啟 res > navigation > nav_graph.xml 檔案。如果尚未選取「Design」分頁標籤,請予以選取。

28c2c94eb97e2f0.png

  1. 選取後即可開啟「Navigation Editor」,以視覺化方式呈現應用程式中的導覽圖。您應該會看到應用程式中已有四個片段。

fdce89b318218ea6.png

  1. 連結導覽圖中的片段目的地。建立從 startFragmentflavorFragment 的動作、從 flavorFragmentpickupFragment 的連線,以及從 pickupFragmentsummaryFragment 的連線。如需更詳細的操作說明,請按照下列後續步驟操作。
  2. 將滑鼠游標懸停在 startFragment 上,直到片段周圍顯示灰色框線,且片段右側邊緣的中心顯示灰色圓圈圖示。按一下圓圈並拖曳至 flavorFragment,然後放開滑鼠。

d014c1b710c1088d.png

  1. 兩個片段之間的箭頭表示已成功連線,因此您將可以從 startFragment 前往 flavorFragment。這就是「導覽」動作,您可以在先前的程式碼研究室中學到。

65c7d993b98c9dea.png

  1. 同樣地,將新增從 flavorFragmentpickupFragment,以及從 pickupFragmentsummaryFragment 的導覽動作。建立導覽動作後,完成的導覽圖看起來會如下所示。

724eb8992a1a9381.png

  1. 您建立的三個新動作應該也會顯示在「Component Tree」窗格中。

e4ee54469f5ff1a4.png

  1. 定義導覽圖時,建議您也指定開始目的地。目前您可以看到 startFragment 旁有一個小型房屋圖示。

739d4ddac561c478.png

這表示 startFragment 將是第一個顯示在 NavHost 中的片段。設定為應用程式所需的預期行為。日後只要在任一片段上按一下滑鼠右鍵,然後選取「Set as Start Destination」選單選項,即可隨時變更起點的位置。

bf3cfa7841476892.png

接下來,您必須新增程式碼,目的是在第一個片段中輕觸按鈕時從 startFragment 前往 flavorFragment,而非顯示 Toast 訊息。以下是起始片段版面配置的參考資料。您在之後的任務中,必須將杯子蛋糕的數量傳遞至口味片段。

867d8e4c72078f76.png

  1. 在「Project」視窗中,依序開啟 app > java > com.example.cupcake > StartFragment Kotlin 檔案。
  2. onViewCreated() 方法中,請留意,點擊事件監聽器設定在三個按鈕上。輕觸各個按鈕時,系統會呼叫 orderCupcake() 方法,並以杯子蛋糕數量 (1、6 或 12 個杯子蛋糕) 做為參數的數量。

參考識別碼:

orderOneCupcake.setOnClickListener { orderCupcake(1) }
orderSixCupcakes.setOnClickListener { orderCupcake(6) }
orderTwelveCupcakes.setOnClickListener { orderCupcake(12) }
  1. orderCupcake() 方法中,將顯示浮動式訊息的程式碼替換成前往口味片段的程式碼。使用 findNavController() 方法取得 NavController,並呼叫此方法的 navigate(),並在傳入動作 ID R.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)
}
  1. 新增「Import」import androidx.navigation.fragment.findNavController,或從 Android Studio 提供的選項中選擇。

2a087f53a77765a6.png

在口味和取貨片段中新增「Navigation」

這項工作與上一個工作類似,您要在其他片段 (口味和取貨片段) 中新增導覽。

3b351067bf4926b7.png

  1. 開啟 app > java > com.example.cupcake > FlavorFragment.kt。請注意,「Next」按鈕點擊事件監聽器內呼叫的方法為 goToNextScreen() 方法。
  2. FlavorFragment.ktgoToNextScreen() 方法中,取代顯示浮動式訊息的程式碼,前往取貨片段。使用動作 ID R.id.action_flavorFragment_to_pickupFragment 並確認此 ID 與 nav_graph.xml. 中宣告的動作相符
fun goToNextScreen() {
    findNavController().navigate(R.id.action_flavorFragment_to_pickupFragment)
}

記得要 import androidx.navigation.fragment.findNavController

  1. 同樣地,在 PickupFragment.ktgoToNextScreen() 方法中,取代現有的程式碼以前往摘要片段。
fun goToNextScreen() {
    findNavController().navigate(R.id.action_pickupFragment_to_summaryFragment)
}

匯入 androidx.navigation.fragment.findNavController

  1. 執行應用程式。確認按鈕可供切換畫面瀏覽。每個片段顯示的資訊可能不完整,但請放心,您會在後續步驟中填入正確的片段。

96b33bf7a5bd8050.png

更新應用程式列中的標題

瀏覽應用程式時,請留意應用程式列中的標題。永遠顯示為杯子蛋糕

根據目前片段的功能提供更相關的標題,有助於改善使用者體驗。

使用 NavController 變更應用程式列 (也稱為動作列) 中各片段的標題,並使用「Up」 (←) 按鈕。

b7657cdc50cfeab0.png

  1. MainActivity.kt 中覆寫 onCreate() 方法來設定導覽控制器。從 NavHostFragment 取得 NavController 的執行個體。
  2. 呼叫 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)
    }
}
  1. 在 Android Studio 顯示提示時新增必要匯入項目。
import android.os.Bundle
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupActionBarWithNavController
  1. 設定每個片段的應用程式列標題。開啟 navigation/nav_graph.xml 並切換至「Code」分頁標籤。
  2. nav_graph.xml 中,修改每個片段目的地的 android:label 屬性。使用已在範例應用程式中宣告的下列字串資源。

如果是起始片段,請使用 @string/app_nameCupcake 值。

如果是口味片段,請使用 @string/choose_flavorChoose Flavor 值。

如果是取貨片段,請使用 @string/choose_pickup_dateChoose Pickup Date 值。

如果是摘要片段,請使用 @string/order_summaryOrder 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>
  1. 執行應用程式。請注意,在您前往各個片段目的地時,應用程式列中的標題會隨之變更。另請注意,應用程式列上會顯示「Up」按鈕 (箭頭 ←)。如果輕觸按鈕,則不會採取任何行動。在下一個程式碼研究室中,您會實作「Up」按鈕行為。

89e0ea37d4146271.png

4. 建立共用 ViewModel

現在要開始在各個片段中填入正確資料。您將使用共用 ViewModel 將應用程式資料儲存在單一 ViewModel 中。應用程式中的多個片段會使用其活動範圍來存取共用的 ViewModel

在大部分生產環境應用程式中,不同片段間共用資料是常見的用途。舉例來說,在杯子蛋糕應用程式的 (本程式碼實驗室) 最終版本中 (請注意下方螢幕截圖),使用者在第一個畫面選取杯子蛋糕數量,系統在第二個畫面依杯子蛋糕的數量計算並顯示價格。同樣地,摘要畫面也使用口味和取貨日期等其他應用程式資料。

3b6a68cab0b9ee2.png

從應用程式功能的角度來看,您可以選擇將這筆訂單資訊儲存在單一 ViewModel 中,即可在此活動中的各片段之間共用。請記得 ViewModelAndroid 架構元件的一部分。在設定變更期間,會保留儲存在 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 類別。系統會分隔瀏覽模型程式碼與其餘使用者介面程式碼 (片段和活動)。根據程式碼功能,將程式碼分隔為不同套件是程式設計的最佳做法。

  1. 在 Android Studio 的「Project」視窗中,以滑鼠右鍵按一下「com.example.cupcake」 >「New」 >「Package」。
  2. 系統會開啟「New Package」對話方塊,將套件命名為 com.example.cupcake.model

d958ee5f3d2aef5a.png

  1. model 套件之下建立 OrderViewModel Kotlin 類別。在「Project」視窗中,以滑鼠右鍵按一下 model 套件,然後選取「New」>「Kotlin File/Class」。在新對話方塊中,提供檔案名稱 OrderViewModel

fc68c1d3861f1cca.png

  1. OrderViewModel.kt 中,變更類別簽名以從 ViewModel 延伸。
import androidx.lifecycle.ViewModel

class OrderViewModel : ViewModel() {

}
  1. OrderViewModel 類別中,將上述討論的屬性新增為 private val
  2. 請將資源型別變更為 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
  1. OrderViewModel 類別中,新增上述方法。在方法中,指派傳遞至可變動屬性中的引數。
  2. 由於這些 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
}
  1. 建構並執行應用程式,確保不會發生編譯錯誤。使用者介面不應出現任何可見的變更。

做得好!現在,您已經開始使用檢視模型。隨著您在應用程式中建構更多功能,就會知道您的類別需要更多屬性和方法,並在類別中加入更多。

如果您在 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>()
  1. StartFragment 類別中,取得共用檢視模型的參照做為類別變數。使用 fragment-ktx 程式庫中的 by activityViewModels() Kotlin 屬性委派。
private val sharedViewModel: OrderViewModel by activityViewModels()

您可能需要匯入以下新資料:

import androidx.fragment.app.activityViewModels
import com.example.cupcake.model.OrderViewModel
  1. 針對 FlavorFragmentPickupFragmentSummaryFragment 類別重複上述步驟,您將在程式碼研究室的後續章節中使用此 sharedViewModel 執行個體。
  2. 返回 StartFragment 類別後,即可使用檢視模型。在 orderCupcake() 方法的開頭,先在共用檢視模型中呼叫 setQuantity() 方法來更新數量,之後再前往口味片段。
fun orderCupcake(quantity: Int) {
    sharedViewModel.setQuantity(quantity)
    findNavController().navigate(R.id.action_startFragment_to_flavorFragment)
}
  1. OrderViewModel 類別中新增下列方法以檢查是否已設定訂單的口味。您將在後續步驟中在 StartFragment 類別中使用此方法。
fun hasNoFlavorSet(): Boolean {
    return _flavor.value.isNullOrEmpty()
}
  1. 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)
}
  1. 建構應用程式以確保不會發生編譯錯誤。但使用者介面並不會出現任何可見的變更。

6. 搭配使用 ViewModel 與資料繫結

接下來,您需要使用資料繫結將檢視模型資料繫結至使用者介面。系統也會根據使用者在使用者介面中的選擇,更新共用檢視模型。

複習資料繫結

請注意,資料繫結程式庫Android Jetpack 的一部分。資料繫結使用宣告式格式,將版面配置中的使用者介面元件繫結至應用程式中的資料來源。簡單來說,資料繫結將資料 (從程式碼) 繫結至檢視 + 檢視表繫結 (將檢視繫結至程式碼)。設定這些繫結,並啟用自動進行更新後,即使您忘記從程式碼手動更新使用者介面,也能降低錯誤發生的機率。

用使用者的選擇來更新品味

  1. 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 ...>

    ...
  1. 同樣地,請針對 fragment_pickup.xmlfragment_summary.xml 重複上率步驟以新增 viewModel 版面配置變數。您將在接下來幾節中使用此變數。此版面配置未使用共用檢視模型,因此您不需要在 fragment_start.xml 中加入這段程式碼。
  2. 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
  1. PickupFragmentSummaryFragment 類別中,針對 onViewCreated() 方法重複上述步驟。
binding?.apply {
    viewModel = sharedViewModel
    ...
}
  1. 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)),但事件監聽器繫結可讓您執行任意資料繫結運算式。

  1. fragment_flavor.xml 中,使用監聽器繫結將事件監聽器新增至圓形按鈕。使用不含參數的 lambda 運算式,並呼叫 viewModelsetFlavor() 方法藉由傳入對應的口味字串資源。
<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>
  1. 執行應用程式,並注意在口味片段中如何依據預設選取的「Vanilla」(香草) 選項。

3095e824b4817b98.png

漂亮!現在可以移到下一個片段。

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」。

現在使用 SimpleDateFormatLocale 來判斷 杯子蛋糕 應用程式的可用取貨日期。

  1. OrderViewModel 類別中新增名為 getPickupOptions() 的函式,以便建立並傳回取貨日期清單。在方法中,建立一個名為 optionsval 變數,然後初始化為 mutableListOf<String>()
private fun getPickupOptions(): List<String> {
   val options = mutableListOf<String>()
}
  1. 使用 SimpleDateFormat 傳遞模式字串 "E MMM d" 和地區設定來建立格式轉換程式字串。在模式字串中,E 代表星期幾,且會剖析為「Tue Dec 10」(12 月 10 日星期四)。
val formatter = SimpleDateFormat("E MMM d", Locale.getDefault())

當 Android Studio 出現提示時,匯入 java.text.SimpleDateFormatjava.util.Locale

  1. 取得 Calendar 執行個體並指派給新的變數,將其設定為 val。此變數會包含目前的日期和時間。而且匯入 java.util.Calendar
val calendar = Calendar.getInstance()
  1. 建置當天日期加上後續三個日期的日期清單。由於這需要 4 個日期選項,請重複此程式碼區塊 4 次。此 repeat 區塊會格式化日期,將其加到日期選項清單中,然後將日曆增加 1 天。
repeat(4) {
    options.add(formatter.format(calendar.time))
    calendar.add(Calendar.DATE, 1)
}
  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
}
  1. 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] (大後天)

  1. fragment_pickup.xml 中,針對 option0 圓形按鈕使用新的版面配置變數 viewModel,以便根據檢視模型中的 date 值來設定 checked 屬性。比較 viewModel.date 屬性與 dateOptions 清單中的第一個字串 (也就是當天日期)。使用 equals 函式進行比較,最終繫結運算式如下所示:

@{viewModel.date.equals(viewModel.dateOptions[0])}

  1. 針對相同的圓形按鈕,使用事件監聽器繫結新增事件監聽器至 onClick 屬性。按一下此按鈕後,viewModel 就會呼叫 setDate(),傳入 dateOptions[0]
  2. 如果是相同的圓形按鈕,請將 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]}"
   ...
   />
  1. 針對其他圓形按鈕重複上述步驟,並據此變更 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]}"
   ... />
  1. 執行應用程式後,當取貨選項可用時就會看到未來幾天。螢幕截圖取決於您的目前日期而有所不同。請注意,依據預設不會選取任何選項。您將在下一個步驟中導入此動作。

b55b3a36e2aa7be6.png

  1. OrderViewModel 類別中,建立名為 resetOrder() 的函式,重設檢視模型中的 MutableLiveData 屬性。將 dateOptions 清單中的目前日期值指派給 _date.value.
fun resetOrder() {
   _quantity.value = 0
   _flavor.value = ""
   _date.value = dateOptions[0]
   _price.value = 0.0
}
  1. 在類別中新增 init 區塊,並從該類別呼叫新方法 resetOrder()
init {
   resetOrder()
}
  1. 從類別中的屬性宣告移除初始值。您目前正在使用 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
  1. 再次執行應用程式。請注意,依據預設會選取今天的日期。

bfe4f1b82977b4bc.png

更新摘要片段以使用檢視模型

現在介紹最後一個片段。訂單摘要片段是用來顯示訂單詳細資料的摘要。在這項工作中,請妥善利用共用檢視模型中的所有訂單資訊,並使用資料繫結來更新螢幕上的訂單詳細資料。

78f510e10d848dd2.png

  1. 請務必在 fragment_summary.xml 中宣告資料檢視模型資料變數 viewModel
<layout ...>

    <data>
        <variable
            name="viewModel"
            type="com.example.cupcake.model.OrderViewModel" />
    </data>

    <ScrollView ...>

    ...
  1. SummaryFragment 中,在onViewCreated() 中確認 binding.viewModel 已初始化。
  2. 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}"
   ... />
  1. 執行並測試應用程式,確認您所選的訂單選項已顯示在訂單摘要中。

7091453fa817b55.png

8. 依據訂單詳細資料計算價格

查看本程式碼研究室的最終應用程式螢幕截圖時,您會注意到價格確實顯示在每個片段 (StartFragment 除外) 上,因此使用者在建立訂單時即可得知價格。

3b6a68cab0b9ee2.png

以下是我們的杯子蛋糕專賣店如何計算價格的規則。

  • 每個杯子蛋糕 $2.00 美元
  • 對於當天取貨,訂單中會再增加 $3.00 美元

因此,6 個杯子蛋糕訂單的價格為 6 個杯子蛋糕 x 每個 $2 美元 = $12 美元。如果同一位使用者想要當天取貨,$3 美元的附加費用會讓訂單總價變成 $15 美元。

更新檢視模型中的價格

若要在應用程式中支援這項功能,請先處理杯子蛋糕的價格,並立即忽略當天取貨費用。

  1. 開啟 OrderViewModel.kt,然後將每杯子蛋糕的價格儲存在變數中。在類別定義之外 (但在匯入陳述式之後),將此變數宣告為檔案頂端的頂層私人常數。使用 const 修飾符,並使用 val 設定為唯讀。
package ...

import ...

private const val PRICE_PER_CUPCAKE = 2.00

class OrderViewModel : ViewModel() {
    ...

請記得,常數值 (在 Kotlin 中以 const 關鍵字標記) 不會變更,而且會在編譯期間知道該值。若要進一步瞭解常數,請參閱 說明文件

  1. 您已經定義了每杯子蛋糕的價格,請建立計算價格的輔助方法。此方法可能是 private,因為只能在此類別中使用。您會在下一個工作中變更價格邏輯,以便納入當天取貨費用。
private fun updatePrice() {
    _price.value = (quantity.value ?: 0) * PRICE_PER_CUPCAKE
}

這段程式碼會將每杯子蛋糕的價格乘以所訂購的杯子蛋糕數量。關於括號中的程式碼,因為 quantity.value 的值可以是空值,所以使用 elvis 運算子 (?:)。elvis 運算子 (?:) 表示左側運算式不是空值時,則使用該運算子。如果左側運算式是空值,則使用 elvis 運算子右側的運算式 (本例中為 0)。

  1. 在相同 OrderViewModel 類別中,在設定數量時更新價格變數。在 setQuantity() 函式中呼叫新函式。
fun setQuantity(numberCupcakes: Int) {
    _quantity.value = numberCupcakes
    updatePrice()
}

將價格屬性繫結到使用者介面

  1. fragment_flavor.xmlfragment_pickup.xmlfragment_summary.xml 的版面配置中,確認已定義 com.example.cupcake.model.OrderViewModel 型別的資料變數 viewModel
<layout ...>

    <data>
        <variable
            name="viewModel"
            type="com.example.cupcake.model.OrderViewModel" />
    </data>

    <ScrollView ...>

    ...
  1. 在每個片段類別的 onViewCreated() 方法中,請務必將片段中的檢視模型物件執行個體繫結至版面配置中的檢視模型資料變數。
binding?.apply {
    viewModel = sharedViewModel
    ...
}
  1. 在每個片段版面配置中,如果在版面配置中有顯示價格,則使用 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>
  1. 執行應用程式。如果您在起始片段中選取「One cupcake」(一個杯子蛋糕),口味片段會顯示「Subtotal 2.0」(小計 2.0)。如果您選取「Six cupcake」(六個杯子蛋糕),口味片段會顯示「Subtotal 12.0」(小計 12.0),以此類推。您稍後會將價格格式化為適當的貨幣格式,因此此行為目前是正常現象。

  1. 現在針對取貨和摘要片段進行類似的變更。在 fragment_pickup.xmlfragment_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. 執行應用程式。確認訂單摘要中顯示的價格是按訂單量 1、6 和 12 個杯子蛋糕正確計算所得。如前所述,預期此刻價格格式不正確 ($2 會顯示為 2.0,$12 會顯示為 12.0)。

當天取貨附加費

在這項工作中,您將導入第二項規則,就是當天取貨會額外收取 $3.00 美元。

  1. OrderViewModel 類別中,針對當天取貨費用定義新頂層私人常數。
private const val PRICE_FOR_SAME_DAY_PICKUP = 3.00
  1. updatePrice() 中,檢查使用者是否選取了當天取貨。檢查檢視模型 (_date.value) 中的日期是否與 dateOptions 清單中的第一個項目相同 (始終是當天)。
private fun updatePrice() {
    _price.value = (quantity.value ?: 0) * PRICE_PER_CUPCAKE
    if (dateOptions[0] == _date.value) {

    }
}
  1. 為了簡化這些計算,請引入暫時變數 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
}
  1. setDate() 方法呼叫 updatePrice() 輔助方法,即可指定當天取貨費用。
fun setDate(pickupDate: String) {
    _date.value = pickupDate
    updatePrice()
}
  1. 執行應用程式,瀏覽應用程式。您會發現,變更取貨日期不會從總價中扣除當天取貨費用。這是因為在檢視模型中價格有所變更,但不會通知繫結版面配置。

2ea8e000fb4e6ec8.png

設定生命週期擁有者以觀測 LiveData

LifecycleOwner 是一個具有 Android 生命週期的類別,例如活動或片段。唯有生命週期擁有者處於有效狀態 (STARTEDRESUMED),LiveData 觀測程式才會觀測應用程式資料的變更。

在應用程式中,LiveData 物件或可觀測資料是檢視模型中的 price 屬性。生命週期擁有者是口味、取貨和摘要片段。LiveData 觀測程式是版面配置檔案中的繫結運算式,當中包含可觀測資料,例如價格。透過資料繫結,可觀測值變更時,也會自動更新其繫結的使用者介面元素。

繫結運算式範例:android:text="@{@string/subtotal_price(viewModel.price)}"

為了自動更新使用者介面元素,您必須在應用程式中建立 binding.lifecycleOwner

與生命週期擁有者的關聯。接下來,您將導入這項動作。

  1. FlavorFragmentPickupFragmentSummaryFragment 類別的 onViewCreated() 方法中,在 binding?.apply 區塊中加入下列內容。這項操作會設定繫結物件的生命週期擁有者。設定生命週期擁有者,應用程式將能觀測 LiveData 物件。
binding?.apply {
    lifecycleOwner = viewLifecycleOwner
    ...
}
  1. 再次執行應用程式。在取貨畫面中,變更取貨日期,並留意自動變更價格方式的差異。且在摘要畫面中正確反映取貨費用。
  2. 請注意,當您選取當天取貨時,訂單價格會增加 $3.00 美元。選取未來日期的價格應是杯子蛋糕數量 x $2.00 美元。

  1. 使用不同杯子蛋糕數量、口味和取貨日期來測試不同情況。現在,您應該會在每個片段上看到檢視模型的更新價格。最棒的是,您不用撰寫額外的 Kotlin 程式碼,就能讓使用者介面每次都更新價格。

f4c0a3c5ea916d03.png

如要完成價格功能的導入,您必須將價格格式設定為當地幣別。

使用 LiveData 轉換來轉換價格的格式

LiveData transformation 方法可讓您針對來源 LiveData 執行資料操縱,並傳回產生的 LiveData 物件。簡單來說,此會將 LiveData 的值轉換為其他值。除非觀測程式觀測到 LiveData 物件,否則不會計算這些轉換。

Transformations.map() 其中一種是轉換函式,此方法採用 LiveData 和函式做為參數。函式會操控來源 LiveData,並傳回可觀測的更新值。

以下列舉幾個可使用 LiveData 轉換的範例:

  • 設定顯示的日期、時間字串等格式
  • 項目清單排序
  • 篩選項目或將項目分組
  • 從清單計算結果,例如所有項目的加總、項目數目、傳回的最後一個項目等等。

在這項工作中,您必須使用 Transformations.map() 方法將設定價格的格式為使用當地幣別。這會將原始價格從十進位值 (LiveData<Double>) 轉換為字串值 (LiveData<String>)。

  1. OrderViewModel 類別中,將幕後屬性類型變更為 LiveData<String>,而不是 LiveData<Double>.。價格的格式將為包含貨幣符號 (例如「$」) 的字串。您將在下一步驟修正初始化錯誤。
private val _price = MutableLiveData<Double>()
val price: LiveData<String>
  1. 使用 Transformations.map() 初始化新變數,並傳入 _price 和 lambda 函式。請在 NumberFormat 類別中使用 getCurrencyInstance() 方法,以將價格轉換成當地幣別格式。轉換程式碼看起來會像這樣。
private val _price = MutableLiveData<Double>()
val price: LiveData<String> = Transformations.map(_price) {
   NumberFormat.getCurrencyInstance().format(it)
}

請匯入androidx.lifecycle.Transformationsjava.text.NumberFormat

  1. 執行應用程式。現在畫面上應會顯示小計及總計的字串格式價格。更容易使用!

1853bd13a07f1bc7.png

  1. 測試是否正常運作。測試範例:訂購一個杯子蛋糕、訂購六個杯子蛋糕、訂購 12 個杯子蛋糕。確認每個螢幕上的價格正確無誤。口味和取貨片段中應顯示「Subtotal $2.00」,而訂單摘要中應顯示「Total $2.00」。也請確認訂單摘要顯示正確的訂單詳細資料。

9. 使用事件監聽器繫結設定點擊事件監聽器

在這項工作中,您將使用事件監聽器繫結來將片段類別中的按鈕點擊事件監聽器繫結至版面配置。

  1. 在版面配置檔案 fragment_start.xml 中,新增名為 startFragment 且型別為 com.example.cupcake.StartFragment 的資料變數。確認片段的套件名稱與應用程式的套件名稱相符。
<layout ...>

    <data>
        <variable
            name="startFragment"
            type="com.example.cupcake.StartFragment" />
    </data>
    ...
    <ScrollView ...>
  1. StartFragment.ktonViewCreated() 方法中,將新的資料變數繫結至片段執行個體。您可以使用 this 關鍵字來存取片段中的片段執行個體。移除 binding?.apply 區塊以及區塊內的程式碼。已完成的方法應如下所示。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    binding?.startFragment = this
}
  1. 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)}"
    ... />
  1. 執行應用程式。請注意,起始片段中的按鈕點擊處理常式運作正常。
  2. 同樣地,您也可以在其他版面配置中加入上述資料變數,以便繫結片段執行個體 fragment_flavor.xmlfragment_pickup.xmlfragment_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 ...>
  1. 在其餘片段類別中的 onViewCreated() 中,刪除可手動設定按鈕上點擊事件監聽器的程式碼。
  2. 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
   }
}
  1. 同樣地,在其他版面配置檔案中,新增事件監聽器繫結運算式至按鈕的 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()}"
    ...>
  1. 執行應用程式以驗證按鈕是否正常運作。您應該不會發現行為變更,但已使用事件監聽器繫結設定點擊事件監聽器!

恭喜您完成程式碼研究室,打造 杯子蛋糕 應用程式!但是應用程式尚未處理完畢。在接下來的程式碼研究室中,您將新增「Cancel」按鈕並修改返回堆疊。您也會瞭解什麼是返回堆疊和其他新主題。到時見!

10. 解決方案程式碼

本程式碼研究室的解決方案程式碼位於下方顯示的專案中。使用 viewmodel 分支版本提取或下載程式碼。

如要取得本程式碼研究室的程式碼,並在 Android Studio 中開啟,請按照下列步驟操作。

取得程式碼

  1. 按一下上面顯示的網址。系統會在瀏覽器中開啟專案的 GitHub 頁面。
  2. 在專案的 GitHub 頁面中,按一下「Code」按鈕開啟對話方塊。

5b0a76c50478a73f.png

  1. 在對話方塊中,按一下「Download ZIP」按鈕,將專案儲存到電腦。等待下載作業完成。
  2. 在電腦中找到該檔案 (可能位於「下載」資料夾中)。
  3. 按兩下解壓縮 ZIP 檔案。這項操作會建立含有專案檔案的新資料夾。

在 Android Studio 中開啟專案

  1. 啟動 Android Studio。
  2. 在「Welcome to Android Studio」視窗中,按一下「Open an existing Android Studio project」

36cc44fcf0f89a1d.png

注意:如果 Android Studio 已開啟,請依序選取「File」>「New」>「Import Project」選單選項。

21f3eec988dcfbe9.png

  1. 在「Import Project」對話方塊中,前往解壓縮專案資料夾所在的位置 (可能位於「下載」資料夾中)。
  2. 按兩下該專案資料夾。
  3. 等待 Android Studio 開啟專案。
  4. 按一下「Run」按鈕 11c34fc5e516fb1c.png 即可建構並執行應用程式。請確認應用程式的建構符合預期。
  5. 在「Project」工具視窗中瀏覽專案檔案,查看應用程式的設定方式。

11. 摘要

  • ViewModelAndroid 架構元件 的一部分,在設定變更期間會保留儲存在 ViewModel 中的應用程式資料。若要在應用程式中加入 ViewModel,請建立新類別,並從 ViewModel 類別擴充該類別。
  • 共用 ViewModel 會將應用程式的資料從多個片段儲存在單一 ViewModel 中 應用程式中的多個片段會使用其活動範圍來存取共用的 ViewModel
  • LifecycleOwner 是一個具有 Android 生命週期的類別,例如活動或片段。
  • 唯有生命週期擁有者處於有效狀態 (STARTEDRESUMED),LiveData 觀測程式才會觀測應用程式資料的變更。
  • 事件監聽器繫結是指在事件發生時 (例如 onClick 事件) 執行的 lambda 運算式。做法類似於方法參照 (例如 textview.setOnClickListener(clickListener)),但事件監聽器繫結可讓您執行任意資料繫結運算式。
  • LiveData transformation 方法可讓您針對來源 LiveData 執行資料操縱,並傳回產生的 LiveData 物件。
  • Android 架構提供了一個名為 SimpleDateFormat 的類別,該類別會以區分地區設定方式來格式化並剖析日期。這允許進行日期格式化 (日期 → 文字) 及剖析 (文字 → 日期)。

12. 瞭解詳情