使用视图构建 Android 应用

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 和任何相关的逻辑或导航。最终结果是一个带有空列表的应用,您可以在其中添加自己的果汁。

d6dc43171ae62047.png 87b2ca7b49e814cb.png 2d630489477e216e.png

2. 获取起始代码

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

3. 创建布局

使用 View 构建应用时,您可以在 Layout 中构建 UI。布局通常使用 XML 声明。这些 XML 布局文件位于资源目录的 res > layout 下。布局包含构成 UI 的组件;这些组件称为 View。XML 语法由标签、元素和属性组成。有关 XML 语法的更多详细信息,请参考 为 Android 创建 XML 布局 Codelab。

在本节中,您将为图中所示的“果汁类型”输入对话框构建 XML 布局。

87b2ca7b49e814cb.png

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

Android studio project pane context pane opened with an option to create a layout resource file.

6adb279d6e74ab13.png

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

请注意,根元素ConstraintLayout。这种布局类型是一个 ViewGroup,它允许您使用约束以灵活的方式定位和调整 View 的大小。 ViewGroup 是一种包含其他 ViewView,这些 View 称为子 View。以下步骤将更详细地介绍此主题,但您可以在使用 ConstraintLayout 构建响应式 UI 中了解更多关于 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 为其他视图提供内边距。准则约束了“果汁类型”标题文本。

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

110cad4ae809e600.png

  1. TextViewid 设置为 header_title
  2. layout_width 设置为 0dp。布局约束最终决定了此 TextView 的宽度。因此,定义宽度只会增加绘制 UI 时的不必要计算;将宽度定义为 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。要实现此结果,您可以通过要约束视图的 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 的放置。请记住,如何约束给定的视图完全取决于您希望 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 提供数据。

  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 Binding 文件自动生成。View Binding 允许您访问和操作在 XML 中声明的 View,您可以在View Binding 文档中阅读更多相关信息。
  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 Binding 后,您可以访问和修改布局中的 ViewonViewCreated() 方法在生命周期中的 onCreateView() 之后调用。onViewCreated() 方法是访问和修改布局中 View 的推荐位置。

  1. 通过调用 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 的导航。

  1. 打开 nav_graph.xml 文件,并确保选中设计选项卡。783cb5d7ff0ba127.png
  2. 点击 93401bf098936c15.png 图标添加新目的地。

d5410c90e408b973.png

  1. 选择 EntryDialogFragment 目的地。此操作在导航图中声明了 entryDialogFragment,使其可用于导航操作。

418feed425072ea4.png

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

  1. 将光标拖动到 trackerFragment 上。选中灰点并将线拖到 entryDialogFragment85decb6fcddec713.png
  2. 导航图设计视图允许您通过选择目的地并点击参数下拉列表旁边的 a0d73140a20e4348.png 图标来声明参数。使用此功能为 entryDialogFragment 添加一个类型为 LongitemId 参数;默认值应为 0L

555cf791f64f62b8.png

840105bd52f300f7.png

请注意,TrackerFragment 持有 Juice 项列表 — 如果您点击其中一项,则会启动 EntryDialogFragment

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

6. 完成 Fragment

利用导航参数中的数据,完成输入对话框。

  1. EntryDialogFragmentonViewCreated() 方法中检索 navArgs()
  2. navArgs() 中检索 itemId
  3. 实现 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()
           )
        }
    }
}
  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 Component 启动输入对话框。输入对话框需要在用户点击浮动操作按钮 (FAB) 时启动。当用户点击某个项时,它还需要启动并传递相应的 ID。

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

TrackerFragment.kt

import androidx.navigation.findNavController


//...

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

//...
  1. 在 navigate 函数中,传递从 tracker 导航到 entry dialog 的 action。

TrackerFragment.kt

//...

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

//...
  1. JuiceListAdapteronEdit() 方法的 lambda 主体中重复此操作,但这次传递 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 上查看