使用视图构建 Android 应用

1. 开始之前

简介

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

先决条件

您需要什么

  • 一台连接互联网的电脑和 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" />

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

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

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. 使用视图创建片段

在 Compose 中,您可以使用 Kotlin 或 Java 声明式地构建布局。您可以通过导航到不同的 Composable 来访问不同的“屏幕”,通常在同一个 Activity 中。使用视图构建应用时,承载 XML 布局的片段取代了 Composable“屏幕”的概念。

在本节中,您将创建一个 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 布局的视图绑定文件自动生成。视图绑定允许您访问和交互与 XML 声明的 视图,您可以在 视图绑定 文档中阅读有关它们的更多信息。
  2. EntryDialogFragment 类中,实现 onCreateView() 函数。顾名思义,此函数为此 Fragment 创建 视图

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 提供了一种简单的方法来渲染不同的可组合项,只需调用它们即可。但是,Fragment 的工作方式有所不同。 导航组件 协调 Fragment 的“目的地”,提供了一种在不同的 Fragment 及其包含的视图之间轻松移动的方法。

使用导航组件协调导航到您的 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() 时,传递来自参数的项目 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. 在 navigate 函数中,传递从跟踪器导航到输入对话框的操作。

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