使用 View 构建 Android 应用

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,以完成界面和起始代码。具体来说,您将构建用于创建新果汁的条目表单,包括界面和任何相关的逻辑或导航。完成后,您将得到一个包含空列表的应用,您可以向其中添加自己的果汁。

d6dc43171ae62047.png 87b2ca7b49e814cb.png 2d630489477e216e.png

2. 获取起始代码

  1. 在 Android Studio 中,打开 basic-android-kotlin-compose-training-juice-tracker 文件夹。
  2. 在 Android Studio 中,打开 Juice Tracker 应用代码。

3. 创建布局

使用 Views 构建应用时,您将在布局中构建界面。布局通常使用 XML 进行声明。这些 XML 布局文件位于资源目录中的 res > layout 下。布局包含组成界面的组件;这些组件称为 View。XML 语法由标记、元素和属性组成。如需详细了解 XML 语法,请参阅为 Android 创建 XML 布局 Codelab。

在本部分中,您将为如图所示的 Type of juice 条目对话框构建 XML 布局。

87b2ca7b49e814cb.png

  1. main > res > layout 目录中创建一个名为 fragment_entry_dialog 的新布局资源文件

打开的 Android Studio 项目窗格上下文窗格,其中显示了用于创建布局资源文件的选项。

6adb279d6e74ab13.png

fragment_entry_dialog.xml 布局包含应用向用户显示的界面组件。

请注意,根元素ConstraintLayout。此类布局是一种 ViewGroup,可让您使用约束条件灵活地设置 View 的位置和大小。ViewGroup 是一种 View,其中包含其他 View(称为子 View)。下面的步骤更详细地介绍了这一主题,但如需详细了解 ConstraintLayout,请参阅使用 ConstraintLayout 构建自适应界面

  1. 创建文件后,在 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>
  1. 将以下准则添加到 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 标题文本。

  1. 创建一个 TextView 元素。此 TextView 表示详情 fragment 的标题。

110cad4ae809e600.png

  1. TextViewid 设置为 header_title
  2. layout_width 设置为 0dp。布局约束条件最终定义此 TextView 的宽度。因此,定义宽度只会在绘制界面时增加不必要的计算;定义 0dp 的宽度可以避免额外的计算。
  3. TextView text 属性设置为 @string/juice_type
  4. 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。

  1. 将标题顶部约束为 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" />
  1. 将结束位置约束为 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 布局并向界面提供数据。

  1. juicetracker 软件包中,新建一个名为 EntryDialogFragment 的类。
  2. 使 EntryDialogFragment 扩展 BottomSheetDialogFragment

EntryDialogFragment.kt

import com.google.android.material.bottomsheet.BottomSheetDialogFragment

class EntryDialogFragment : BottomSheetDialogFragment() {
}

DialogFragment 是用于显示浮动对话框的 FragmentBottomSheetDialogFragment 继承自 DialogFragment 类,但其会显示与固定在屏幕底部的界面等宽的工作表。这种方法与上图中的设计相符。

  1. 重新构建项目,这会使系统自动生成基于 fragment_entry_dialog 的 View 绑定文件。通过 View 绑定,您可以访问 XML 声明的 View 并与之互动。如需了解详情,请参阅 View 绑定文档。
  2. 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

  1. 返回通过膨胀 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
}
  1. onCreateView() 函数之外(但在 EntryDialogFragment 类内),创建 EntryViewModel 的实例。
  2. 实现 onViewCreated() 函数。

膨胀 View 绑定后,您可以访问和修改布局中的 View。系统会在生命周期中的 onCreateView() 之后调用 onViewCreated() 方法。建议您使用 onViewCreated() 方法在布局中访问和修改 View

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

  1. 打开 nav_graph.xml 文件,并确保已选择 Design 标签页。783cb5d7ff0ba127.png
  2. 点击 93401bf098936c15.png 图标以添加新的目的地。

d5410c90e408b973.png

  1. 选择 EntryDialogFragment 目的地。此操作将在导航图中声明 entryDialogFragment,使其可供导航操作访问。

418feed425072ea4.png

您需要从 TrackerFragment 启动 EntryDialogFragment。因此,导航操作需要完成此任务。

  1. 将光标拖动到 trackerFragment 上方。选择灰点,然后将线条拖动到 entryDialogFragment85decb6fcddec713.png
  2. 借助 nav_graph Design 视图,您可以通过以下方式声明目的地参数:选择相应目的地,然后点击 Arguments 下拉菜单旁边的 a0d73140a20e4348.png 图标。使用此功能将类型为 LongitemId 参数添加到 entryDialogFragment;默认值应为 0L

555cf791f64f62b8.png

840105bd52f300f7.png

请注意,TrackerFragment 包含 Juice 项列表。如果您点击其中一个项,EntryDialogFragment 就会启动。

  1. 重新构建项目。现在可以在 EntryDialogFragment 中访问 itemId 参数了。

6. 完成 Fragment

使用导航参数中的数据,完成条目对话框。

  1. EntryDialogFragmentonViewCreated() 方法中检索 navArgs()
  2. navArgs() 检索 itemId
  3. 实现 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()
           )
        }
    }
}
  1. 保存数据后,使用 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。

  1. 在 FAB 的 onClickListener() 中,对导航控制器调用 navigate()

TrackerFragment.kt

import androidx.navigation.findNavController

//...

binding.fab.setOnClickListener { fabView ->
   fabView.findNavController().navigate(
   )
}

//...
  1. 在导航函数中,传递从跟踪器导航到条目对话框的操作。

TrackerFragment.kt

//...

binding.fab.setOnClickListener { fabView ->
   fabView.findNavController().navigate(
TrackerFragmentDirections.actionTrackerFragmentToEntryDialogFragment()
   )
}

//...
  1. 在 lambda 正文中为 JuiceListAdapter 中的 onEdit() 方法重复此操作,但这次要传递 Juiceid

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