1. 开始之前
简介
到目前为止,您已经了解了如何使用 Compose 构建 Android 应用。这是一件好事!Compose 是一个非常强大的工具,可以简化开发过程。然而,Android 应用并非总是使用声明式 UI 构建。Compose 在 Android 应用历史上是一个非常新的工具。Android UI 最初是使用 View 构建的。因此,在继续作为 Android 开发者之旅时,您极有可能遇到 View。在本 Codelab 中,您将学习 Compose 出现之前如何构建 Android 应用的基础知识——使用 XML、View、View Binding 和 Fragment。
前提条件
- 完成 单元 7 的 Android Basics with Compose 课程内容。
所需工具
- 一台可上网的计算机和 Android Studio
- 一台设备或模拟器
- Juice Tracker 应用的起始代码
您将构建什么
在本 Codelab 中,您将完成 Juice Tracker 应用。此应用允许您通过构建包含详细项的列表来跟踪重要的果汁。您将添加和修改 Fragment 和 XML 来完成 UI 和起始代码。具体来说,您将构建用于创建新果汁的输入表单,包括 UI 和任何相关的逻辑或导航。最终结果是一个带有空列表的应用,您可以在其中添加自己的果汁。
2. 获取起始代码
- 在 Android Studio 中,打开
basic-android-kotlin-compose-training-juice-tracker
文件夹。 - 在 Android Studio 中打开 Juice Tracker 应用代码。
3. 创建布局
使用 View
构建应用时,您可以在 Layout 中构建 UI。布局通常使用 XML 声明。这些 XML 布局文件位于资源目录的 res > layout 下。布局包含构成 UI 的组件;这些组件称为 View
。XML 语法由标签、元素和属性组成。有关 XML 语法的更多详细信息,请参考 为 Android 创建 XML 布局 Codelab。
在本节中,您将为图中所示的“果汁类型”输入对话框构建 XML 布局。
- 在 main > res > layout 目录下创建名为
fragment_entry_dialog
的新布局资源文件。
fragment_entry_dialog.xml
布局包含应用向用户显示的 UI 组件。
请注意,根元素是 ConstraintLayout
。这种布局类型是一个 ViewGroup
,它允许您使用约束以灵活的方式定位和调整 View 的大小。 ViewGroup
是一种包含其他 View
的 View
,这些 View 称为子 View。以下步骤将更详细地介绍此主题,但您可以在使用 ConstraintLayout 构建响应式 UI 中了解更多关于 ConstraintLayout
的信息。
- 创建文件后,在
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
为其他视图提供内边距。准则约束了“果汁类型”标题文本。
- 创建一个
TextView
元素。此TextView
代表详情 Fragment 的标题。
- 将
TextView
的id
设置为header_title
。 - 将
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 以声明方式构建布局。您可以通过导航到不同的可组合项(通常在同一 Activity 中)来访问不同的“屏幕”。使用 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 Binding 文件自动生成。View Binding 允许您访问和操作在 XML 中声明的View
,您可以在View Binding 文档中阅读更多相关信息。 - 在
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 Binding 后,您可以访问和修改布局中的 View
。 onViewCreated()
方法在生命周期中的 onCreateView()
之后调用。onViewCreated()
方法是访问和修改布局中 View
的推荐位置。
- 通过调用
FragmentEntryDialogBinding
上的bind()
方法创建一个 View Binding 实例。
此时,您的代码应如下例所示
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)
}
}
您可以通过 Binding 访问和设置 View。例如,您可以通过 setText()
方法设置一个 TextView
。
binding.name.setText("Apple juice")
输入对话框 UI 用于用户创建新项,但您也可以使用它来修改现有项。因此,Fragment 需要检索被点击的项。Navigation Component 便于导航到 EntryDialogFragment
并检索被点击的项。
EntryDialogFragment
尚未完成,但不用担心!现在,转到下一部分以详细了解如何在包含 View
的应用中使用导航组件。
5. 修改 Navigation Component
在本节中,您将使用 Navigation Component 启动输入对话框并在适用时检索项。
Compose 提供了仅通过调用即可渲染不同可组合项的机会。然而,Fragment 的工作方式不同。Navigation Component 协调 Fragment 的“目的地”,提供一种在不同 Fragment 及其包含的 View 之间轻松移动的方式。
使用 Navigation Component 协调到您的 EntryDialogFragment
的导航。
- 打开
nav_graph.xml
文件,并确保选中设计选项卡。 - 点击
图标添加新目的地。
- 选择
EntryDialogFragment
目的地。此操作在导航图中声明了entryDialogFragment
,使其可用于导航操作。
您需要从 TrackerFragment
启动 EntryDialogFragment
。因此,需要一个导航操作来完成此任务。
- 将光标拖动到
trackerFragment
上。选中灰点并将线拖到entryDialogFragment
。 - 导航图设计视图允许您通过选择目的地并点击参数下拉列表旁边的
图标来声明参数。使用此功能为
entryDialogFragment
添加一个类型为Long
的itemId
参数;默认值应为0L
。
请注意,TrackerFragment
持有 Juice
项列表 — 如果您点击其中一项,则会启动 EntryDialogFragment
。
- 重新构建项目。
itemId
参数现在可在EntryDialogFragment
中访问。
6. 完成 Fragment
利用导航参数中的数据,完成输入对话框。
- 在
EntryDialogFragment
的onViewCreated()
方法中检索navArgs()
。 - 从
navArgs()
中检索itemId
。 - 实现
saveButton
以使用ViewModel
保存新建/修改的果汁。
回想输入对话框 UI,默认颜色值为红色。目前,将其作为一个占位符传递。
调用 saveJuice()
时,从 args 传递 item 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
独有,您可以自行实现这些代码。尝试实现其余功能。作为最后的手段,您可以参考此 Codelab 的解决方案代码。
7. 启动输入对话框
最后一个任务是使用 Navigation Component 启动输入对话框。输入对话框需要在用户点击浮动操作按钮 (FAB) 时启动。当用户点击某个项时,它还需要启动并传递相应的 ID。
- 在 FAB 的
onClickListener()
中,在导航控制器上调用navigate()
。
TrackerFragment.kt
import androidx.navigation.findNavController
//...
binding.fab.setOnClickListener { fabView ->
fabView.findNavController().navigate(
)
}
//...
- 在 navigate 函数中,传递从 tracker 导航到 entry dialog 的 action。
TrackerFragment.kt
//...
binding.fab.setOnClickListener { fabView ->
fabView.findNavController().navigate(
TrackerFragmentDirections.actionTrackerFragmentToEntryDialogFragment()
)
}
//...
- 在
JuiceListAdapter
的onEdit()
方法的 lambda 主体中重复此操作,但这次传递Juice
的id
。
TrackerFragment.kt
//...
onEdit = { drink ->
findNavController().navigate(
TrackerFragmentDirections.actionTrackerFragmentToEntryDialogFragment(drink.id)
)
},
//...
8. 获取解决方案代码
要下载完成的 Codelab 代码,您可以使用以下 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 上查看。