1. 事前準備
簡介
到目前為止,您已學到有關使用 Compose 建構 Android 應用程式的所有概念。實在太棒了!Compose 是一項可以簡化開發程序的強大工具。不過,Android 應用程式不一定要以宣告式 UI 建構。自有 Android 應用程式以來,Compose 是非常近期才問世的工具,Android UI 最初是以 View 建構。因此,在您繼續 Android 開發之旅的同時,還是很有可能會接觸到 View。本程式碼研究室將說明一些基本概念,介紹在沒有 Compose 的時代,如何使用 XML、View、View 繫結和 Fragment 建構 Android 應用程式。
必要條件:
- 完成「Android 基本概念:使用 Compose」課程的單元 7。
軟硬體需求
- 已安裝 Android Studio 且連上網路的電腦
- 裝置或模擬器
- Juice Tracker 應用程式的範例程式碼
建構項目
在本程式碼研究室中,您將製作 Juice Tracker 應用程式。這個應用程式能建立含有詳細品項資料的清單,方便您記錄重要的果汁品項。您需要新增及修改片段和 XML,完成 UI 和範例程式碼。具體來說,您將建構用來建立新果汁的輸入表單,包括 UI 和任何相關的邏輯或導覽機制。完成的應用程式包含一份空白清單,可讓您自行加入果汁品項。
2. 取得範例程式碼
- 在 Android Studio 中開啟
basic-android-kotlin-compose-training-juice-tracker
資料夾。 - 在 Android Studio 中開啟 Juice Tracker 應用程式程式碼。
3. 建立版面配置
使用 Views
建構應用程式時,您必須在版面配置中建構 UI。版面配置通常是以 XML 宣告。這些 XML 版面配置檔案位於「res」>「layout」下的資源目錄中。版面配置包含構成 UI 的元件,這些元件稱為 View
。XML 語法由標記、元素和屬性組成。如要進一步瞭解 XML 語法,請參閱「為 Android 建立 XML 版面配置」程式碼研究室。
在本節中,您將為圖中的「Type of juice」輸入對話方塊建立 XML 版面配置。
- 依序前往「main」>「res」>「layout」目錄,建立名為
fragment_entry_dialog
的新版面配置資源檔案。
fragment_entry_dialog.xml
版面配置包含應用程式向使用者顯示的 UI 元件。
請注意,根元素為 ConstraintLayout
。這類版面配置是一種 ViewGroup
,可讓您運用限制條件,靈活調整檢視區塊的位置和大小。ViewGroup
是一種 View
,其中包含其他 View
,稱為子 View
。以下步驟將深入探討這個主題,但如要進一步瞭解 ConstraintLayout
,請參閱「使用 ConstraintLayout 打造回應式 UI」。
- 建立檔案後,請在
ConstraintLayout
中定義應用程式名稱空間。
fragment_entry_dialog.xml
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.constraintlayout.widget.ConstraintLayout>
- 在
ConstraintLayout
中加入下列規範。
fragment_entry_dialog.xml
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_left"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="16dp" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_middle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_begin="16dp" />
這些 Guideline
可做為其他檢視區塊的邊框間距。這些規範限制了「Type of juice」標題文字。
- 建立
TextView
元素。此TextView
代表詳細資料片段的標題。
- 將
TextView
設為header_title
的id
。 - 將
layout_width
設為0dp
。版面配置限制最終會定義這個TextView
的寬度。因此,定義寬度只會在 UI 繪圖期間增加不必要的計算。定義0dp
的寬度可避免額外的計算作業。 - 將
TextView text
屬性設為@string/juice_type
。 - 將
textAppearance
設為@style/TextAppearance.MaterialComponents.Headline5
。
fragment_entry_dialog.xml
<TextView
android:id="@+id/header_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/juice_type"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5" />
最後,您需定義限制條件。與使用維度做為限制的 Guideline
不同,規範本身會限制這個 TextView
。為此,您可以參照要用來限制檢視區塊的 Guideline
ID。
- 將標題頂端限制在
guideline_top
的底部。
fragment_entry_dialog.xml
<TextView
android:id="@+id/header_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/juice_type"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
app:layout_constraintTop_toBottomOf="@+id/guideline_top" />
- 將結尾限制在
guideline_middle
的開頭,開頭則限制在guideline_left
的開頭,完成TextView
的放置作業。請記住,如何限制特定檢視區塊,完全取決於您希望呈現的 UI 外觀。
fragment_entry_dialog.xml
<TextView
android:id="@+id/header_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/juice_type"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
app:layout_constraintTop_toBottomOf="@+id/guideline_top"
app:layout_constraintEnd_toStartOf="@+id/guideline_middle"
app:layout_constraintStart_toStartOf="@+id/guideline_left" />
請試著根據螢幕截圖建構其餘的 UI。您可以在解決方案中找到完成的 fragment_entry_dialog.xml
檔案。
4. 使用 View 建立 Fragment
您可以在 Compose 中使用 Kotlin 或 Java,以宣告式方式建構版面配置。只要前往不同的可組合函式,即可存取不同「畫面」,可組合函式通常位於同一個活動中。使用 View 建構應用程式時,代管 XML 版面配置的 Fragment 會取代可組合函式「畫面」的概念。
在本節中,您將建立 Fragment
來代管 fragment_entry_dialog
版面配置,並將資料提供給 UI。
- 在
juicetracker
套件中,建立名為EntryDialogFragment
的新類別。 - 讓
EntryDialogFragment
擴充BottomSheetDialogFragment
。
EntryDialogFragment.kt
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
class EntryDialogFragment : BottomSheetDialogFragment() {
}
DialogFragment
是顯示浮動對話方塊的 Fragment
。BottomSheetDialogFragment
繼承自 DialogFragment
類別,但會顯示工作表,該工作表的畫面寬度與固定在螢幕底部的畫面寬度相同。這個方法與先前描述的設計相符。
- 重建專案,讓系統根據
fragment_entry_dialog
版面配置自動產生 View 繫結檔案。View 繫結可讓您存取 XML 宣告的View
並與其進行互動,詳情請參閱 View 繫結說明文件。 - 在
EntryDialogFragment
類別中,實作onCreateView()
函式。顧名思義,此函式會為這個Fragment
建立View
。
EntryDialogFragment.kt
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return super.onCreateView(inflater, container, savedInstanceState)
}
onCreateView()
函式會傳回 View
,但目前不會傳回有用的 View
。
- 傳回因加載
FragmentEntryDialogViewBinding
而產生的View
,而非傳回super.onCreateView()
。
EntryDialogFragment.kt
import com.example.juicetracker.databinding.FragmentEntryDialogBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return FragmentEntryDialogBinding.inflate(inflater, container, false).root
}
- 在
onCreateView()
函式外 (但在EntryDialogFragment
類別中) 建立EntryViewModel
的例項。 - 實作
onViewCreated()
函式。
加載 View 繫結後,您可以存取及修改版面配置中的 View
。系統會在生命週期中的 onCreateView()
之後呼叫 onViewCreated()
方法。如要存取及修改版面配置中的 View
,建議使用 onViewCreated()
方法。
- 對
FragmentEntryDialogBinding
呼叫bind()
方法,以建立 View 繫結的例項。
此時,您的程式碼應如以下範例所示:
EntryDialogFragment.kt
import androidx.fragment.app.viewModels
import com.example.juicetracker.ui.AppViewModelProvider
import com.example.juicetracker.ui.EntryViewModel
class EntryDialogFragment : BottomSheetDialogFragment() {
private val entryViewModel by viewModels<EntryViewModel> { AppViewModelProvider.Factory }
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return FragmentEntryDialogBinding.inflate(inflater, container, false).root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val binding = FragmentEntryDialogBinding.bind(view)
}
}
您可以透過繫結來存取及設定 View。舉例來說,您可以透過 setText()
方法設定 TextView
。
binding.name.setText("Apple juice")
輸入對話方塊 UI 是個可讓使用者建立新項目的地方,但您也可以用它來修改現有項目。因此,Fragment 需擷取點選的項目。Navigation 元件可協助您前往 EntryDialogFragment
並擷取點選的項目。
EntryDialogFragment
尚未完成,但請放心!請繼續參閱下一節,進一步瞭解如何透過 View
在應用程式中使用 Navigation 元件。
5. 修改 Navigation 元件
在本節中,您會使用 Navigation 元件啟動輸入對話方塊並擷取項目 (如適用)。
只要使用 Compose 呼叫這些不同的可組合函式,就可以對它們進行算繪。不過,Fragment 的運作方式有所不同。Navigation 元件會協調 Fragment 的「目的地」,讓您輕鬆在不同的 Fragment 和所包含的 View 之間移動。
請使用 Navigation 元件,協調導覽至 EntryDialogFragment
的動作。
- 開啟
nav_graph.xml
檔案,並確定已選取「Design」分頁標籤。 - 按一下 圖示即可新增目的地。
- 選取
EntryDialogFragment
目的地。此動作會在導覽圖中宣告entryDialogFragment
,使其可供導覽動作存取。
您需從 TrackerFragment
啟動 EntryDialogFragment
。因此,導覽動作需要完成這項工作。
- 將游標拖曳至
trackerFragment
上。選取灰點,然後將線條拖曳至entryDialogFragment
。 - 您可以透過 nav_graph 設計檢視畫面宣告目的地的引數,方法是選取目的地,然後按一下「Arguments」下拉式選單旁邊的 圖示。請使用這項功能將
Long
類型的itemId
引數新增至entryDialogFragment
,預設值應為0L
。
請注意,TrackerFragment
包含 Juice
項目的清單,只要按一下其中一個項目,EntryDialogFragment
就會啟動。
- 重建專案。現在就可以在
EntryDialogFragment
中存取itemId
引數了。
6. 完成 Fragment
使用導覽引數中的資料,完成輸入對話方塊。
- 在
EntryDialogFragment
的onViewCreated()
方法中擷取navArgs()
。 - 從
navArgs()
擷取itemId
。 - 實作
saveButton
,使用ViewModel
儲存新的/已修改的果汁。
回想一下,輸入對話方塊 UI 預設的色彩值為紅色。請暫時將其以預留位置形式傳遞。
呼叫 saveJuice()
時,傳遞從引數獲得的項目 ID。
EntryDialogFragment.kt
import androidx.navigation.fragment.navArgs
import com.example.juicetracker.data.JuiceColor
class EntryDialogFragment : BottomSheetDialogFragment() {
//...
var selectedColor: JuiceColor = JuiceColor.Red
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val binding = FragmentEntryDialogBinding.bind(view)
val args: EntryDialogFragmentArgs by navArgs()
val juiceId = args.itemId
binding.saveButton.setOnClickListener {
entryViewModel.saveJuice(
juiceId,
binding.name.text.toString(),
binding.description.text.toString(),
selectedColor.name,
binding.ratingBar.rating.toInt()
)
}
}
}
- 資料儲存完畢後,請使用
dismiss()
方法關閉對話方塊。
EntryDialogFragment.kt
class EntryDialogFragment : BottomSheetDialogFragment() {
//...
var selectedColor: JuiceColor = JuiceColor.Red
//...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val binding = FragmentEntryDialogBinding.bind(view)
val args: EntryDialogFragmentArgs by navArgs()
binding.saveButton.setOnClickListener {
entryViewModel.saveJuice(
juiceId,
binding.name.text.toString(),
binding.description.text.toString(),
selectedColor.name,
binding.ratingBar.rating.toInt()
)
dismiss()
}
}
}
請注意,上述程式碼中的 EntryDialogFragment
並不完整。您仍須實作一些內容,例如將現有 Juice
資料填入欄位 (如適用)、從 colorSpinner
中選取顏色、實作 cancelButton
等。不過,此程式碼不是 Fragment
的專屬程式碼,您可以自行實作。請嘗試實作其他功能。萬一仍然卡關,再參考本程式碼研究室的解決方案程式碼。
7. 啟動輸入對話方塊
最後一項工作是使用 Navigation 元件啟動輸入對話方塊。當使用者按一下懸浮動作按鈕 (FAB) 時,輸入對話方塊就需啟動。此外,此對話方塊也需在使用者點選項目時,啟動並傳遞對應的 ID。
- 在懸浮動作按鈕 (FAB) 的
onClickListener()
中,呼叫導覽控制器上的navigate()
。
TrackerFragment.kt
import androidx.navigation.findNavController
//...
binding.fab.setOnClickListener { fabView ->
fabView.findNavController().navigate(
)
}
//...
- 在導覽函式中,傳遞從追蹤器前往輸入對話方塊的動作。
TrackerFragment.kt
//...
binding.fab.setOnClickListener { fabView ->
fabView.findNavController().navigate(
TrackerFragmentDirections.actionTrackerFragmentToEntryDialogFragment()
)
}
//...
- 在 lambda 主體中,針對
JuiceListAdapter
內的onEdit()
方法重複這項操作,但這次請傳遞Juice
的id
。
TrackerFragment.kt
//...
onEdit = { drink ->
findNavController().navigate(
TrackerFragmentDirections.actionTrackerFragmentToEntryDialogFragment(drink.id)
)
},
//...
8. 取得解決方案程式碼
完成程式碼研究室後,如要下載當中用到的程式碼,您可以使用這些 git 指令:
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-juice-tracker.git $ cd basic-android-kotlin-compose-training-juice-tracker $ git checkout views
另外,您也可以下載存放區為 ZIP 檔案,然後解壓縮並在 Android Studio 中開啟。
如要查看程式碼解答,請前往 GitHub。