使用视图构建 Android 应用

1. 开始之前

简介

到目前为止,您已经学习了如何使用 Compose 构建 Android 应用。这是件好事!Compose 是一个非常强大的工具,可以简化开发流程。但是,Android 应用并不总是使用声明式 UI 构建的。Compose 是 Android 应用历史上一个非常新的工具。Android UI 最初是使用视图构建的。因此,在您继续作为 Android 开发者的旅程时,您很可能遇到视图。在本 Codelab 中,您将学习 Android 应用在 Compose 之前是如何构建的基本知识 - 使用 XML、视图、视图绑定和片段。

先决条件

  • 完成通过 第 7 单元 的 Android 基础知识与 Compose 课程。

您需要什么

  • 一台具有互联网访问权限和 Android Studio 的计算机
  • 一个设备或模拟器
  • Juice Tracker 应用的入门代码

您将构建的内容

在本 Codelab 中,您将完成 Juice Tracker 应用。此应用让您可以通过构建一个包含详细项目的列表来跟踪值得注意的果汁。您将添加和修改片段和 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. 创建布局

在使用 视图 构建应用时,您将在 布局 内构建 UI。布局通常使用 XML 声明。这些 XML 布局文件位于资源目录下的 **res > layout** 中。布局包含构成 UI 的组件;这些组件被称为 视图。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,它允许您使用约束以灵活的方式定位和调整视图的大小。ViewGroup 是一种包含其他 视图(称为子项或子 视图)的 视图 类型。以下步骤将更详细地介绍此主题,但您可以在 使用 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" />

这些 准则 用作其他视图的填充。准则约束了“果汁类型”标题文本。

  1. 创建一个 TextView 元素。此 TextView 表示详细信息片段的标题。

110cad4ae809e600.png

  1. TextViewid 设置为 header_title
  2. layout_width 设置为 0dp。布局约束最终定义了此 TextView 的宽度。因此,定义宽度只会为 UI 绘制添加不必要的计算;定义 0dp 的宽度可以避免额外的计算。
  3. TextViewtext 属性设置为 @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" />

最后,您需要定义约束。与使用尺寸作为约束的 准则 不同,准则本身约束了此 TextView。为了实现此结果,您可以引用要约束视图的 准则 的 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. 使用视图创建片段

在 Compose 中,您使用 Kotlin 或 Java 声明式地构建布局。您可以通过导航到不同的 Composable(通常位于同一个活动内)来访问不同的“屏幕”。在使用视图构建应用时,托管 XML 布局的片段将取代 Composable“屏幕”的概念。

在本节中,您将创建一个 片段 来托管 fragment_entry_dialog 布局,并为 UI 提供数据。

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

EntryDialogFragment.kt

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


class EntryDialogFragment : BottomSheetDialogFragment() {
}

DialogFragment 是一个显示浮动对话框的 片段BottomSheetDialogFragment 继承自 DialogFragment 类,但会显示一个与屏幕宽度相同的表格,固定在屏幕底部。这种方法与之前显示的设计相匹配。

  1. 重建项目,这将导致基于 fragment_entry_dialog 布局的视图绑定文件自动生成。视图绑定让您可以访问和交互与 XML 声明的 视图,您可以在 视图绑定 文档中了解更多信息。
  2. EntryDialogFragment 类中,实现 onCreateView() 函数。顾名思义,此函数将为该 片段 创建 视图

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() 函数返回一个 视图,但现在它没有返回有用的 视图

  1. 返回通过膨胀 FragmentEntryDialogViewBinding 生成的 视图,而不是返回 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() 函数。

膨胀视图绑定后,您可以访问和修改布局中的 视图onViewCreated() 方法在生命周期中的 onCreateView() 之后调用。onViewCreated() 方法是访问和修改布局中 视图 的推荐位置。

  1. 通过在 FragmentEntryDialogBinding 上调用 bind() 方法来创建一个视图绑定的实例。

此时,您的代码应类似于以下示例

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)
    }
}

您可以通过绑定来访问和设置视图。例如,您可以通过 setText() 方法设置 TextView

binding.name.setText("Apple juice")

条目对话框 UI 用作用户创建新项目的场所,但您也可以使用它来修改现有项目。因此,片段需要检索被点击的项目。导航组件有助于导航到 EntryDialogFragment 并检索被点击的项目。

EntryDialogFragment 尚未完成,但别担心!现在,继续下一节,了解有关在使用 视图 的应用中使用导航组件的更多信息。

5. 修改导航组件

在本节中,您将使用导航组件来启动条目对话框,并在适用时检索项目。

Compose 提供了通过简单地调用它们来渲染不同 Composable 的机会。但是,片段的工作原理有所不同。导航组件 协调片段“目的地”,提供了一种在不同片段及其包含的视图之间轻松移动的方法。

使用导航组件来协调导航到您的 EntryDialogFragment

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

d5410c90e408b973.png

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

418feed425072ea4.png

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

  1. 将光标拖动到 trackerFragment 上。选择灰色圆点并将线拖动到 entryDialogFragment 上。85decb6fcddec713.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() 时传递来自参数的项目 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. 启动输入对话框

最后一步是使用导航组件启动输入对话框。输入对话框需要在用户点击浮动操作按钮 (**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. 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 上查看.