1. 始める前に
はじめに
ここまで、Compose で Android アプリを作成する方法を学んできました。それは良いことです。Compose は、開発プロセスを簡素化できる非常に強力なツールです。しかし、Android アプリは必ずしも宣言型 UI で作成されているわけではありません。Android アプリの歴史において、Compose はごく最近のツールです。もともと、Android UI はビューを使用して作成されていました。そのため、Android デベロッパーとしての歩みを進めていくと、ビューに遭遇する可能性が高くなります。この Codelab では、Compose 以前の Android アプリの作成方法の基礎(XML、ビュー、ビュー バインディング、Fragment)を学びます。
前提条件:
- ユニット 7 で「Compose での Android の基礎」コースワークを完了していること。
必要なもの
- Android Studio がインストールされた、インターネットに接続できるパソコン。
- デバイスまたはエミュレータ。
- Juice Tracker アプリのスターター コード。
作成するアプリの概要
この Codelab では、Juice Tracker アプリを完成させます。このアプリは、詳細なアイテムで構成されるリストを作成することで、注目のジュースを追跡できるというものです。Fragment と XML を追加して変更し、UI とスターター コードを完成させます。具体的には、UI と関連するロジックや Navigation を含む、新しいジュースを作るための入力フォームを作成します。その結果、独自のジュースを追加できる空のリストを持ったアプリができあがります。
2. スターター コードを取得する
- Android Studio で
basic-android-kotlin-compose-training-juice-tracker
フォルダを開きます。 - Android Studio で Juice Tracker アプリコードを開きます。
3. レイアウトを作成する
Views
でアプリを作成する場合は、Layout 内に UI を構築します。Layout は通常、XML を使用して宣言します。宣言した XML レイアウト ファイルは、リソース ディレクトリの [res] > [layout] に配置されます。レイアウトには、UI を構成するコンポーネントが含まれています。これらのコンポーネントが View
と呼ばれるものです。XML 構文は、タグ、要素、属性で構成されています。XML 構文の詳細については、Android の XML レイアウトを作成するの Codelab をご覧ください。
このセクションでは [Type of juice] 入力ダイアログの XML レイアウトを作成します。
- [main] > [res] > [layout] ディレクトリに、
fragment_entry_dialog
という名前の新しいレイアウト リソース ファイルを作成します。
fragment_entry_dialog.xml
レイアウトには、アプリがユーザーに表示する UI コンポーネントが含まれています。
ルート要素が ConstraintLayout
であることに注目してください。このタイプのレイアウトは、制約を使用してビューの位置とサイズを柔軟に調整できる ViewGroup
です。ViewGroup
は、他の View
(子または子 View
)を含む View
の一種です。次のステップでは、このトピックについて詳しく説明しますが、ConstraintLayout
の詳細については、ConstraintLayout でレスポンシブ UI を作成するをご覧ください。
- ファイルを作成したら、
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>
ConstraintLayout
に次の Guidelines を追加します。
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
は、他のビューのパディングとして機能します。Guidelines では「Type of juice」というヘッダー テキストが制約されています。
TextView
要素を作成します。このTextView
は、詳細フラグメントのタイトルを表します。
TextView
のid
をheader_title
に設定します。layout_width
を0dp
に設定します。最終的には、レイアウト制約がこのTextView
の幅を定義します。そのため、幅を定義しても、UI の描画中に不要な計算処理が発生するだけです。0dp
の幅を定義すれば余分な計算処理がなくなります。TextView text
属性を@string/juice_type
に設定します。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 を参照します。
- ヘッダーの上部を
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" />
- 終点を
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. ビューで Fragment を作成する
Compose では、Kotlin または Java を使用してレイアウトを宣言的に作成します。別の「画面」にアクセスするには、通常は同じアクティビティ内にある、別のコンポーザブルに移動します。ビューでアプリを作成する場合は、XML レイアウトをホストする Fragment が、Composable の「画面」のコンセプトに代わるものとなります。
このセクションでは、fragment_entry_dialog
レイアウトをホストして UI にデータを提供する Fragment
を作成します。
juicetracker
パッケージで、EntryDialogFragment
という名前の新しいクラスを作成します。EntryDialogFragment
がBottomSheetDialogFragment
を拡張するようにします。
EntryDialogFragment.kt
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
class EntryDialogFragment : BottomSheetDialogFragment() {
}
DialogFragment
は、フローティング ダイアログを表示する Fragment
です。BottomSheetDialogFragment
は DialogFragment
クラスを継承していますが、画面下部に固定された画面幅のシートを表示します。この方法は、前述の設計に合わせたものです。
- プロジェクトを再ビルドします。これにより、
fragment_entry_dialog
レイアウトに基づくビュー バインディング ファイルが自動生成されます。ビュー バインディングを使用すると、XML で宣言されたView
にアクセスして操作できます。詳細については、ビュー バインディングのドキュメントをご覧ください。 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
を返しません。
super.onCreateView()
を返す代わりに、FragmentEntryDialogViewBinding
のインフレートで生成されたView
を返します。
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
}
onCreateView()
関数外、かつEntryDialogFragment
クラス内で、EntryViewModel
のインスタンスを作成します。onViewCreated()
関数を実装します。
ビュー バインディングをインフレートすると、レイアウト内の View
にアクセスして変更できるようになります。onViewCreated()
メソッドが、このライフサイクルで onCreateView()
の後に呼び出されます。onViewCreated()
メソッドは、レイアウト内の View
にアクセスして変更する場合におすすめの場所です。
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 は、ユーザーが新しいアイテムを作成する場所ですが、既存のアイテムを変更するためにも使用できます。そのため、Fragment はクリックされたアイテムを取得する必要があります。Navigation コンポーネントにより、EntryDialogFragment
への移動とクリックされたアイテムの取得が容易になります。
EntryDialogFragment
は未完成ですが、心配無用です。次のセクションに進んで、View
を使用するアプリで Navigation コンポーネントを使用する方法を学びましょう。
5. Navigation コンポーネントを変更する
このセクションでは、入力ダイアログを起動するためと、アイテムを取得するため(該当する場合)に、Navigation コンポーネントを使用します。
Compose では、呼び出すだけでさまざまなコンポーザブルをレンダリングする機会があります。しかし、Fragment の動作は異なります。Navigation コンポーネントは、Fragment の「デスティネーション」を調整し、Fragment 間とそれに含まれるビューの間を簡単に移動できるようにします。
Navigation コンポーネントを使用して、EntryDialogFragment
へのナビゲーションを調整します。
nav_graph.xml
ファイルを開き、[Design] タブが選択されていることを確認します。- アイコンをクリックして、新しいデスティネーションを追加します。
EntryDialogFragment
デスティネーションを選択します。このアクションは、nav graph でentryDialogFragment
を宣言し、ナビゲーション アクションからアクセスできるようにするものです。
TrackerFragment
から EntryDialogFragment
を起動する必要があります。そのためには、このタスクをナビゲーション アクションで実行する必要があります。
trackerFragment
の上にカーソルを移動します。灰色の点を選択して、線をentryDialogFragment
にドラッグします。- nav_graph デザインビューでは、デスティネーションを選択し、[Arguments] プルダウンの横にある アイコンをクリックして、デスティネーションの引数を宣言できます。この機能を使用して、
Long
型のitemId
引数をentryDialogFragment
に追加します。デフォルト値は0L
です。
TrackerFragment
は Juice
アイテムのリストを保持します。これらのアイテムのいずれかをクリックすると、EntryDialogFragment
が起動します。
- プロジェクトを再ビルドします。
EntryDialogFragment
でitemId
引数にアクセスできるようになりました。
6. Fragment を完成させる
ナビゲーション引数のデータを使用して、入力ダイアログを完成させます。
EntryDialogFragment
のonViewCreated()
メソッドでnavArgs()
を取得します。navArgs()
からitemId
を取得します。ViewModel
を使用して、新規または変更されたジュースを保存するsaveButton
を実装します。
入力ダイアログ 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()
)
}
}
}
- データを保存したら、
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 を渡す必要があります。
- FAB の
onClickListener()
で、nav コントローラのnavigate()
を呼び出します。
TrackerFragment.kt
import androidx.navigation.findNavController
//...
binding.fab.setOnClickListener { fabView ->
fabView.findNavController().navigate(
)
}
//...
- navigate 関数に、トラッカーから入力ダイアログに移動するアクションを渡します。
TrackerFragment.kt
//...
binding.fab.setOnClickListener { fabView ->
fabView.findNavController().navigate(
TrackerFragmentDirections.actionTrackerFragmentToEntryDialogFragment()
)
}
//...
- このラムダ本体でのアクションを
JuiceListAdapter
のonEdit()
メソッドについても繰り返しますが、今回はJuice
のid
を渡します。
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 で表示します。