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 查看。