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 で表示します。