使用 View 建構 Android 應用程式

1. 事前準備

簡介

到目前為止,您已學到有關使用 Compose 建構 Android 應用程式的所有概念。實在太棒了!Compose 是一項可以簡化開發程序的強大工具。不過,Android 應用程式不一定要以宣告式 UI 建構。自有 Android 應用程式以來,Compose 是非常近期才問世的工具,Android UI 最初是以 View 建構。因此,在您繼續 Android 開發之旅的同時,還是很有可能會接觸到 View。本程式碼研究室將說明一些基本概念,介紹在沒有 Compose 的時代,如何使用 XML、View、View 繫結和 Fragment 建構 Android 應用程式。

必要條件:

  • 完成「Android 基本概念:使用 Compose」課程的單元 7

軟硬體需求

  • 已安裝 Android Studio 且連上網路的電腦
  • 裝置或模擬器
  • Juice Tracker 應用程式的範例程式碼

建構項目

在本程式碼研究室中,您將製作 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. 建立版面配置

使用 Views 建構應用程式時,您必須在版面配置中建構 UI。版面配置通常是以 XML 宣告。這些 XML 版面配置檔案位於「res」>「layout」下的資源目錄中。版面配置包含構成 UI 的元件,這些元件稱為 View。XML 語法由標記、元素和屬性組成。如要進一步瞭解 XML 語法,請參閱「為 Android 建立 XML 版面配置」程式碼研究室。

在本節中,您將為圖中的「Type of juice」輸入對話方塊建立 XML 版面配置。

87b2ca7b49e814cb.png

  1. 依序前往「main」>「res」>「layout」目錄,建立名為 fragment_entry_dialog 的新版面配置資源檔案

在 Android studio 專案窗格內開啟的內容窗格,其中顯示了建立版面配置資源檔案的選項。

6adb279d6e74ab13.png

fragment_entry_dialog.xml 版面配置包含應用程式向使用者顯示的 UI 元件。

請注意,根元素ConstraintLayout。這類版面配置是一種 ViewGroup,可讓您運用限制條件,靈活調整檢視區塊的位置和大小。ViewGroup 是一種 View,其中包含其他 View,稱為子 View。以下步驟將深入探討這個主題,但如要進一步瞭解 ConstraintLayout,請參閱「使用 ConstraintLayout 打造回應式 UI」。

  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 可做為其他檢視區塊的邊框間距。這些規範限制了「Type of juice」標題文字。

  1. 建立 TextView 元素。此 TextView 代表詳細資料片段的標題。

110cad4ae809e600.png

  1. TextView 設為 header_titleid
  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,以宣告式方式建構版面配置。只要前往不同的可組合函式,即可存取不同「畫面」,可組合函式通常位於同一個活動中。使用 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 繫結檔案。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() 方法。如要存取及修改版面配置中的 View,建議使用 onViewCreated() 方法。

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

輸入對話方塊 UI 是個可讓使用者建立新項目的地方,但您也可以用它來修改現有項目。因此,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 設計檢視畫面宣告目的地的引數,方法是選取目的地,然後按一下「Arguments」下拉式選單旁邊的 a0d73140a20e4348.png 圖示。請使用這項功能將 Long 類型的 itemId 引數新增至 entryDialogFragment,預設值應為 0L

555cf791f64f62b8.png

840105bd52f300f7.png

請注意,TrackerFragment 包含 Juice 項目的清單,只要按一下其中一個項目,EntryDialogFragment 就會啟動。

  1. 重建專案。現在就可以在 EntryDialogFragment 中存取 itemId 引數了。

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 的專屬程式碼,您可以自行實作。請嘗試實作其他功能。萬一仍然卡關,再參考本程式碼研究室的解決方案程式碼。

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. 取得解決方案程式碼

完成程式碼研究室後,如要下載當中用到的程式碼,您可以使用這些 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