เกี่ยวกับ Codelab นี้
1 准备工作
简介
至此,您已经全面学习了如何使用 Compose 构建 Android 应用。这非常有用!Compose 是一款可以简化开发流程的强大工具。不过,Android 应用并非总是使用声明性界面构建的。在 Android 应用的历史长河中,Compose 是一款非常新的工具。Android 界面最初是使用 View 构建的。因此,随着您继续从事 Android 开发工作,您很可能会遇到 View。在此 Codelab 中,您将学习与在 Compose 问世之前如何构建 Android 应用相关的基础知识(使用 XML、View、View 绑定和 Fragment)。
前提条件:
- 已完成“Android 之 Compose 开发基础”课程作业的前 7 个单元。
所需条件
- 一台连接到互联网并安装了 Android Studio 的电脑
- 一台设备或模拟器
- Juice Tracker 应用的起始代码
构建内容
在此 Codelab 中,您将完成 Juice Tracker 应用。借助这款应用,您可以构建一个包含详细项的列表,以便跟踪各种重要的果汁。您将添加并修改 Fragment 和 XML,以完成界面和起始代码。具体来说,您将构建用于创建新果汁的条目表单,包括界面和任何相关的逻辑或导航。完成后,您将得到一个包含空列表的应用,您可以向其中添加自己的果汁。
2 获取起始代码
- 在 Android Studio 中,打开
basic-android-kotlin-compose-training-juice-tracker
文件夹。 - 在 Android Studio 中,打开 Juice Tracker 应用代码。
3 创建布局
使用 Views
构建应用时,您将在布局中构建界面。布局通常使用 XML 进行声明。这些 XML 布局文件位于资源目录中的 res > layout 下。布局包含组成界面的组件;这些组件称为 View
。XML 语法由标记、元素和属性组成。如需详细了解 XML 语法,请参阅为 Android 创建 XML 布局 Codelab。
在本部分中,您将为如图所示的 Type of juice 条目对话框构建 XML 布局。
- 在 main > res > layout 目录中创建一个名为
fragment_entry_dialog
的新布局资源文件。
fragment_entry_dialog.xml
布局包含应用向用户显示的界面组件。
请注意,根元素是 ConstraintLayout
。此类布局是一种 ViewGroup
,可让您使用约束条件灵活地设置 View 的位置和大小。ViewGroup
是一种 View
,其中包含其他 View
(称为子 View
)。下面的步骤更详细地介绍了这一主题,但如需详细了解 ConstraintLayout
,请参阅使用 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
将用作其他 View 的内边距。这些准则将约束 Type of juice 标题文本。
- 创建一个
TextView
元素。此TextView
表示详情 fragment 的标题。
- 将
TextView
的id
设置为header_title
。 - 将
layout_width
设置为0dp
。布局约束条件最终定义此TextView
的宽度。因此,定义宽度只会在绘制界面时增加不必要的计算;定义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
。为了实现此结果,您可以引用要用来约束 View 的 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
的放置。请注意,给定 View 的约束方式完全取决于您预期的界面外观。
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" />
尝试根据屏幕截图构建界面的其余部分。您可以在解决方案中找到完整的 fragment_entry_dialog.xml
文件。
4 创建包含 View 的 Fragment
在 Compose 中,您可以使用 Kotlin 或 Java 以声明方式构建布局。通过导航到不同的可组合项(通常在同一 activity 中),您可以访问不同的“界面”。使用 View 构建应用时,托管 XML 布局的 Fragment 将取代可组合项“界面”的概念。
在本部分中,您将创建一个 Fragment
来托管 fragment_entry_dialog
布局并向界面提供数据。
- 在
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()
方法。建议您使用 onViewCreated()
方法在布局中访问和修改 View
。
- 对
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")
条目对话框界面是供用户新建项的地方,但您也可以使用它来修改现有项。因此,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 Design 视图,您可以通过以下方式声明目的地参数:选择相应目的地,然后点击 Arguments 下拉菜单旁边的
图标。使用此功能将类型为
Long
的itemId
参数添加到entryDialogFragment
;默认值应为0L
。
请注意,TrackerFragment
包含 Juice
项列表。如果您点击其中一个项,EntryDialogFragment
就会启动。
- 重新构建项目。现在可以在
EntryDialogFragment
中访问itemId
参数了。
6 完成 Fragment
使用导航参数中的数据,完成条目对话框。
- 在
EntryDialogFragment
的onViewCreated()
方法中检索navArgs()
。 - 从
navArgs()
检索itemId
。 - 实现
saveButton
以使用ViewModel
保存新的/经过修改的果汁。
回想一下默认颜色值为 red 的条目对话框界面。目前,将其作为占位符进行传递。
调用 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
独有的,您可以自行实现此代码。尝试实现其余功能。万不得已时,您可以参考此 Codelab 的解决方案代码。
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 获取解决方案代码
如需下载完成后的 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 查看。