1. 始める前に
前の Codelab では、Room 永続ライブラリ(SQLite データベース上の抽象化レイヤ)を使用してアプリデータを保存する方法を学習しました。この Codelab では、Inventory アプリに機能を追加し、Room を使用して SQLite データベースのデータの読み取り、表示、更新、削除を行う方法について学習します。RecyclerView を使用してデータベースのデータを表示し、基となるデータベースのデータが変更されたときに表示データを自動的に更新します。
前提条件
- Room ライブラリを使用して SQLite データベースを作成し、操作する方法の知識。
- エンティティ クラス、DAO クラス、データベース クラスを作成する方法の知識。
- データ アクセス オブジェクト(DAO)を使用して Kotlin 関数を SQL クエリにマッピングする方法の知識。
RecyclerViewでリストアイテムを表示する方法の知識。- このユニットで前の Codelab「Room を使用してデータを永続化する」を受講している。
学習内容
- SQLite データベースからエンティティを読み取り、表示する方法。
- Room ライブラリを使用して SQLite データベースのエンティティの更新と削除を行う方法。
作成するアプリの概要
- インベントリ アイテムのリストを表示する Inventory アプリを作成します。このアプリは、Room を使用してアプリ データベースのアイテムを更新、編集、削除できます。
2. スターター アプリの概要
この Codelab では、前の Codelab で扱った Inventory アプリの解答コードをスターター コードとして使用します。スターター アプリはすでに、Room 永続ライブラリを使用してデータを保存できる状態になっています。ユーザーは [Add Item] 画面を使用してアプリ データベースにデータを追加できます。
注: 現在のバージョンのスターター アプリでは、データベースに保存されている日付は表示されません。
この Codelab ではアプリを拡張し、Room ライブラリを使用して、データの読み取りと表示、データベースのエンティティの更新と削除を行えるようにします。
この Codelab のスターター コードをダウンロードする
このスターター コードは前の Codelab の解答コードと同じものです。
この Codelab のコードを取得して Android Studio で開くには、以下の手順に沿って操作します。
コードを取得する
- 指定された URL をクリックします。プロジェクトの GitHub ページがブラウザで開きます。
- プロジェクトの GitHub ページで、[Code] ボタンをクリックすると、ダイアログが表示されます。

- ダイアログで、[Download ZIP] をクリックして、プロジェクトをパソコンに保存します。ダウンロードが完了するまで待ってください。
- パソコンに保存したファイルを見つけます([ダウンロード] フォルダなど)。
- ZIP ファイルをダブルクリックして展開します。プロジェクト ファイルが入った新しいフォルダが作成されます。
Android Studio でプロジェクトを開く
- Android Studio を起動します。
- [Welcome to Android Studio] ウィンドウで [Open an existing Android Studio project] をクリックします。

注: Android Studio がすでに開いている場合は、メニューから [File] > [New] > [Import Project] を選択します。

- [Import Project] ダイアログで、展開したプロジェクト フォルダがある場所([ダウンロード] フォルダなど)に移動します。
- そのプロジェクト フォルダをダブルクリックします。
- Android Studio でプロジェクトが開くまで待ちます。
- 実行ボタン
をクリックして、アプリをビルドし、実行します。期待どおりに動作することを確認します。 - [Project] ツール ウィンドウでプロジェクト ファイルを見て、アプリがどのように実装されているかを確認します。
3.RecyclerView を追加する
このタスクではアプリに RecyclerView を追加して、データベースに格納されているデータを表示します。
ヘルパー関数を追加して価格を書式設定する
最終的なアプリのスクリーンショットを次に示します。

価格が通貨形式で表示されています。double 型の値を目的の通貨形式に変換するには、Item クラスに拡張関数を追加します。
拡張関数
Kotlin には、クラスを継承する、またはクラスの既存の定義を変更することなく、新しい関数をクラスに追加する機能が用意されています。つまり、ソースコードにアクセスせずに、既存のクラスに関数を追加できます。これは「拡張機能」という特別な宣言を介して行います。
たとえば、自分では変更できないサードパーティ ライブラリのクラスについて、新しい関数を記述できます。このような関数は、元のクラスのメソッドであるかのように、通常の方法で呼び出すことができます。こうした関数を「拡張関数」といいます(既存のクラスの新しいプロパティを定義する「拡張プロパティ」もありますが、これはこの Codelab の範囲外です)。
拡張関数が実際にクラスを変更することはありませんが、そのクラスのオブジェクトに対して関数を呼び出す際、ドット表記を使用できます。
たとえば次のコード スニペットには Square というクラスがあります。このクラスには、辺を表すプロパティと、正方形の面積を計算する関数があります。Square.perimeter() という拡張関数に注目してください。関数名の先頭に、操作するクラスの名前が付いています。この関数の中で、Square クラスのパブリック プロパティを参照できます。
main() 関数での拡張関数の使い方について確認します。作成した拡張関数 perimeter() は、その Square クラス内の通常の関数として呼び出されます。
例:
class Square(val side: Double){
fun area(): Double{
return side * side;
}
}
// Extension function to calculate the perimeter of the square
fun Square.perimeter(): Double{
return 4 * side;
}
// Usage
fun main(args: Array<String>){
val square = Square(5.5);
val perimeterValue = square.perimeter()
println("Perimeter: $perimeterValue")
val areaValue = square.area()
println("Area: $areaValue")
}
このステップでは、アイテムの価格を通貨形式の文字列に書式設定します。一般に、データを書式設定するだけのためにデータを表すエンティティ クラスを変更することはせず(単一責任の原則を参照)、拡張関数を追加します。
Item.ktで、クラス定義の下に、パラメータを受け取らずにStringを返すItem.getFormattedPrice()という拡張関数を追加します。クラス名と、関数名のドット表記に注目してください。
fun Item.getFormattedPrice(): String =
NumberFormat.getCurrencyInstance().format(itemPrice)
Android Studio のプロンプトが表示されたら、java.text.NumberFormat をインポートします。
ListAdapter を追加する
このステップでは、RecyclerView にリスト アダプターを追加します。リスト アダプターの実装については以前の Codelab で説明済みのため、手順の概要のみを次に示します。参考までに、完成した ItemListAdapter ファイルをこのステップの最後に掲載しています。この Codelab での Room のコンセプトについて理解を深めるためにご利用ください。
com.example.inventoryパッケージで、ItemListAdapterという Kotlin クラスを追加します。コンストラクタ パラメータとしてonItemClicked()という関数を渡します。これはパラメータとしてItemオブジェクトを受け取ります。ListAdapterを拡張するように、ItemListAdapterクラスのシグネチャを変更します。ItemとItemListAdapter.ItemViewHolderをパラメータとして渡します。- コンストラクタ パラメータ
DiffCallbackを追加します。ListAdapterは、これを使用してリスト内の変更点を把握します。 - 必要なメソッド
onCreateViewHolder()とonBindViewHolder()をオーバーライドします。 - RecyclerView が必要とする場合、
onCreateViewHolder()メソッドは新しいViewHolderを返します。 onCreateViewHolder()メソッド内で新しいViewを作成し、自動生成されたバインディング クラスItemListItemBindingを使用してitem_list_item.xmlレイアウト ファイルからインフレートします。onBindViewHolder()メソッドを実装します。getItem()メソッドを使用し、位置を渡して現在のアイテムを取得します。itemViewにクリック リスナーを設定し、リスナー内で関数onItemClicked()を呼び出します。ItemViewHolderクラスを定義し、RecyclerView.ViewHolder.から拡張します。bind()関数をオーバーライドし、Itemオブジェクトを渡します。- コンパニオン オブジェクトを定義します。コンパニオン オブジェクト内で、
DiffCallbackというDiffUtil.ItemCallback<Item>()型のvalを定義します。必要なメソッドareItemsTheSame()とareContentsTheSame()をオーバーライドして定義します。
完成したクラスは次のようになります。
package com.example.inventory
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.example.inventory.data.Item
import com.example.inventory.data.getFormattedPrice
import com.example.inventory.databinding.ItemListItemBinding
/**
* [ListAdapter] implementation for the recyclerview.
*/
class ItemListAdapter(private val onItemClicked: (Item) -> Unit) :
ListAdapter<Item, ItemListAdapter.ItemViewHolder>(DiffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
return ItemViewHolder(
ItemListItemBinding.inflate(
LayoutInflater.from(
parent.context
)
)
)
}
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
val current = getItem(position)
holder.itemView.setOnClickListener {
onItemClicked(current)
}
holder.bind(current)
}
class ItemViewHolder(private var binding: ItemListItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: Item) {
}
}
companion object {
private val DiffCallback = object : DiffUtil.ItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem === newItem
}
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem.itemName == newItem.itemName
}
}
}
}
完成したアプリ(この Codelab の最後にある解答アプリ)のインベントリ リスト画面をご覧ください。すべてのリスト要素に、インベントリ アイテムの名前、通貨形式の価格、現在の在庫数が表示されています。これまでのステップでは、item_list_item.xml レイアウト ファイルと 3 つの TextView を使用して行を作成しました。次のステップでは、これらの TextView にエンティティの詳細をバインドします。

ItemListAdapter.ktで、bind()関数をItemViewHolderクラスに実装します。itemNameTextView をitem.itemNameにバインドします。getFormattedPrice()拡張関数を使用して通貨形式の価格を取得し、itemPriceTextView にバインドします。quantityInStock値をStringに変換し、itemQuantityTextView にバインドします。完成したメソッドは次のようになります。
fun bind(item: Item) {
binding.apply {
itemName.text = item.itemName
itemPrice.text = item.getFormattedPrice()
itemQuantity.text = item.quantityInStock.toString()
}
}
Android Studio のプロンプトが表示されたら、com.example.inventory.data.getFormattedPrice をインポートします。
ListAdapter を使用する
このタスクでは InventoryViewModel と ItemListFragment を更新し、前のステップで作成したリスト アダプターを使用してアイテムの詳細を画面に表示します。
InventoryViewModelクラスの先頭で、データベースのアイテムについて、LiveData<List<Item>>型のallItemsというvalを作成します。エラーはこの後すぐに修正するため、気にする必要はありません。
val allItems: LiveData<List<Item>>
Android Studio のプロンプトが表示されたら、androidx.lifecycle.LiveData をインポートします。
itemDaoに対してgetItems()を呼び出し、allItemsに割り当てます。getItems()関数はFlowを返します。データをLiveData値として使用するために、asLiveData()関数を使用します。完成した定義は次のようになります。
val allItems: LiveData<List<Item>> = itemDao.getItems().asLiveData()
Android Studio のプロンプトが表示されたら、androidx.lifecycle.asLiveData をインポートします。
ItemListFragmentで、クラスの先頭にInventoryViewModel型のviewModelという不変プロパティprivateを宣言します。byデリゲートを使用して、プロパティの初期化をactivityViewModelsクラスに引き渡します。InventoryViewModelFactoryコンストラクタを渡します。
private val viewModel: InventoryViewModel by activityViewModels {
InventoryViewModelFactory(
(activity?.application as InventoryApplication).database.itemDao()
)
}
Android Studio の要求に応じて androidx.fragment.app.activityViewModels をインポートします。
- 引き続き
ItemListFragment内で、関数onViewCreated()までスクロールします。super.onViewCreated()呼び出しの下で、adapterというvalを宣言します。何も渡さずにデフォルトのコンストラクタItemListAdapter{}を使用して、新しいadapterプロパティを初期化します。 - 次のように、新しく作成した
adapterをrecyclerViewにバインドします。
val adapter = ItemListAdapter {
}
binding.recyclerView.adapter = adapter
- 引き続き
onViewCreated()内で、アダプターを設定した後、オブザーバーをallItemsにアタッチして、データの変更をリッスンします。 - オブザーバー内で、
adapterに対してsubmitList()を呼び出し、新しいリストを渡します。これにより RecyclerView が、リストに含まれる新しいアイテムで更新されます。
viewModel.allItems.observe(this.viewLifecycleOwner) { items ->
items.let {
adapter.submitList(it)
}
}
- 完成した
onViewCreated()メソッドが次のようになっていることを確認します。アプリを実行します。アプリのデータベースにアイテムが保存されていれば、インベントリ リストが表示されます。リストが空の場合は、インベントリ アイテムをアプリ データベースに追加します。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val adapter = ItemListAdapter {
}
binding.recyclerView.adapter = adapter
viewModel.allItems.observe(this.viewLifecycleOwner) { items ->
items.let {
adapter.submitList(it)
}
}
binding.recyclerView.layoutManager = LinearLayoutManager(this.context)
binding.floatingActionButton.setOnClickListener {
val action = ItemListFragmentDirections.actionItemListFragmentToAddItemFragment(
getString(R.string.add_fragment_title)
)
this.findNavController().navigate(action)
}
}

4. アイテムの詳細を表示する
このタスクでは、エンティティの詳細を読み取って [Item Details] 画面に表示します。主キー(アイテム id)を使用して Inventory アプリのデータベースから名前、価格、数量などの詳細情報を読み取り、レイアウト ファイル fragment_item_detail.xml を使用して [Item Details] 画面に表示します。レイアウト ファイル fragment_item_detail.xml はあらかじめデザインされており、アイテムの詳細を表示する 3 つの TextView が含まれています。

このタスクでは次の手順を実施します。
- RecyclerView にクリック ハンドラを追加して、アプリを [Item Details] 画面に移動する。
ItemListFragmentフラグメントで、データベースからデータを取得して表示する。- TextView を ViewModel データにバインドする。
クリック ハンドラを追加する
ItemListFragmentで、onViewCreated()関数までスクロールしてアダプターの定義を更新します。- コンストラクタ パラメータとしてラムダを
ItemListAdapter{}に追加します。
val adapter = ItemListAdapter {
}
- ラムダ内で、
actionというvalを作成します。初期化エラーはこの後すぐに修正します。
val adapter = ItemListAdapter {
val action
}
ItemListFragmentDirectionsに対してactionItemListFragmentToItemDetailFragment()メソッドを呼び出し、アイテムidを渡します。返されたNavDirectionsオブジェクトをactionに割り当てます。
val adapter = ItemListAdapter {
val action = ItemListFragmentDirections.actionItemListFragmentToItemDetailFragment(it.id)
}
action定義の下で、this.findNavController()を使用してNavControllerインスタンスを取得し、navigate()を呼び出してactionを渡します。アダプターの定義は次のようになります。
val adapter = ItemListAdapter {
val action = ItemListFragmentDirections.actionItemListFragmentToItemDetailFragment(it.id)
this.findNavController().navigate(action)
}
- アプリを実行します。
RecyclerViewでアイテムをクリックします。アプリが [Item Details] 画面に移動します。詳細が空白になっています。ボタンをタップしても何も起こりません。

後のステップで、エンティティの詳細を [Item Details] 画面に表示し、[SELL] ボタンや [DELETE] ボタンに機能を追加します。
アイテムの詳細を取得する
このステップでは、InventoryViewModel に新しい関数を追加して、アイテム id に基づいてデータベースからアイテムの詳細を取得します。次のステップでは、この関数を使用してエンティティの詳細を [Item Details] 画面に表示します。
InventoryViewModelで、アイテム ID にIntを受け取ってLiveData<Item>を返す、retrieveItem()という関数を追加します。戻り値エラーはこの後すぐに修正します。
fun retrieveItem(id: Int): LiveData<Item> {
}
- 新しい関数内で、
itemDaoに対してgetItem()を呼び出し、パラメータidを渡します。getItem()関数はFlowを返します。Flow値をLiveDataとして使用するには、asLiveData()関数を呼び出して、retrieveItem()関数の戻り値として使用します。完成した関数は次のようになります。
fun retrieveItem(id: Int): LiveData<Item> {
return itemDao.getItem(id).asLiveData()
}
データを TextView にバインドする
このステップでは、ItemDetailFragment で ViewModel インスタンスを作成し、ViewModel データを [Item Details] 画面の TextView にバインドします。また、ViewModel のデータにオブザーバーをアタッチして、基になるデータベースのデータが変更された場合に画面上のインベントリ リストが更新されるようにします。
ItemDetailFragmentで、Itemエンティティ型のitemという可変プロパティを追加します。このプロパティは、1 つのエンティティに関する情報を格納するために使用します。このプロパティは後で初期化されるため、先頭にlateinitを付けます。
lateinit var item: Item
Android Studio のプロンプトが表示されたら、com.example.inventory.data.Item をインポートします。
ItemDetailFragmentクラスの先頭で、InventoryViewModel型のviewModelというprivate不変プロパティを宣言します。byデリゲートを使用して、プロパティの初期化をactivityViewModelsクラスに引き渡します。InventoryViewModelFactoryコンストラクタを渡します。
private val viewModel: InventoryViewModel by activityViewModels {
InventoryViewModelFactory(
(activity?.application as InventoryApplication).database.itemDao()
)
}
Android Studio のプロンプトが表示されたら、androidx.fragment.app.activityViewModels をインポートします。
- 引き続き
ItemDetailFragmentで、bind()というprivate関数を作成します。これはItemエンティティのインスタンスをパラメータとして受け取り、何も返しません。
private fun bind(item: Item) {
}
bind()関数を実装します。これは、ItemListAdapterで行ったことと同様です。itemNameTextView のtextプロパティをitem.itemNameに設定します。itemプロパティに対してgetFormattedPrice()を呼び出して価格値を書式設定し、itemPriceTextView のtextプロパティに設定します。quantityInStockをStringに変換し、itemQuantityTextView のtextプロパティに設定します。
private fun bind(item: Item) {
binding.itemName.text = item.itemName
binding.itemPrice.text = item.getFormattedPrice()
binding.itemCount.text = item.quantityInStock.toString()
}
- 次に示すように、コードブロックに
apply{}スコープ関数を使用するようにbind()関数を更新します。
private fun bind(item: Item) {
binding.apply {
itemName.text = item.itemName
itemPrice.text = item.getFormattedPrice()
itemCount.text = item.quantityInStock.toString()
}
}
- 引き続き
ItemDetailFragmentで、onViewCreated()をオーバーライドします。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
- これまでのステップで、
ItemListFragmentからItemDetailFragmentにナビゲーション引数としてアイテム ID を渡しました。onViewCreated()内で、スーパークラス関数の呼び出しの下にidという不変変数を作成します。ナビゲーション引数を取得してこの新しい変数に割り当てます。
val id = navigationArgs.itemId
- 次に、この
id変数を使用してアイテムの詳細を取得します。onViewCreated()内で、viewModelに対してretrieveItem()関数を呼び出し、idを渡します。viewLifecycleOwnerとラムダを渡して、戻り値にオブザーバーをアタッチします。
viewModel.retrieveItem(id).observe(this.viewLifecycleOwner) {
}
- ラムダ内で、データベースから取得した
Itemエンティティを含むパラメータとしてselectedItemを渡します。ラムダ関数本文で、selectedItemの値をitemに割り当てます。bind()関数を呼び出してitemを渡します。完成した関数は次のようになります。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val id = navigationArgs.itemId
viewModel.retrieveItem(id).observe(this.viewLifecycleOwner) { selectedItem ->
item = selectedItem
bind(item)
}
}
- アプリを実行します。[Inventory] 画面でリスト要素をクリックすると、[Item Details] 画面が表示されます。今度は空白画面ではなく、インベントリ データベースから取得したエンティティの詳細が表示されています。
|
|
- [SELL] ボタン、[DELETE] ボタン、FAB をタップします。何も起こりません。これらのボタンの機能は次のタスクで実装します。
5. アイテム販売機能を実装する
このタスクでは、アプリの機能を拡張し、販売機能を実装します。このステップの大まかな流れは次のとおりです。
- ViewModel に関数を追加して、エンティティを更新できるようにする。
- 新しいメソッドを作成し、数量を減らしてアプリ データベースのエンティティを更新できるようにする。
- [SELL] ボタンにクリック リスナーを追加する。
- 数量がゼロの場合は [SELL] ボタンを無効にする。
コーディングしましょう。
InventoryViewModelで、updateItem()というプライベート関数を追加します。これはエンティティ クラスItemのインスタンスを受け取り、何も返しません。
private fun updateItem(item: Item) {
}
- 新しいメソッド
updateItem()を実装します。ItemDaoクラスからupdate()サスペンド メソッドを呼び出すには、viewModelScopeを使用してコルーチンを起動します。起動ブロック内で、itemDaoに対してupdate()関数を呼び出し、itemを渡します。完成したメソッドは、次のようになります。
private fun updateItem(item: Item) {
viewModelScope.launch {
itemDao.update(item)
}
}
- 引き続き
InventoryViewModel内で、sellItem()という別のメソッドを追加します。これはItemエンティティ クラスのインスタンスを受け取り、何も返しません。
fun sellItem(item: Item) {
}
sellItem()関数内で、if条件を追加してitem.quantityInStockが0より大きいかどうかを確認します。
fun sellItem(item: Item) {
if (item.quantityInStock > 0) {
}
}
if ブロック内で、データクラスの copy() 関数を使用してエンティティを更新します。
データクラス: copy()
copy() 関数は、データクラスのすべてのインスタンスにデフォルトで提供されます。この関数は、一部のプロパティを変更し、それ以外のプロパティは変更せずに、オブジェクトをコピーするために使用します。
たとえば、次に示す User クラスとそのインスタンス jack について考えてみましょう。age プロパティの更新のみで新しいインスタンスを作成する場合、その実装は次のようになります。
例
// Data class
data class User(val name: String = "", val age: Int = 0)
// Data class instance
val jack = User(name = "Jack", age = 1)
// A new instance is created with its age property changed, rest of the properties unchanged.
val olderJack = jack.copy(age = 2)
InventoryViewModelのsellItem()関数に戻ります。ifブロック内で、newItemという新しい不変プロパティを作成します。itemインスタンスに対してcopy()関数を呼び出し、更新されたquantityInStockを渡します。これにより、在庫が1減少します。
val newItem = item.copy(quantityInStock = item.quantityInStock - 1)
newItemの定義の下で、updateItem()関数を呼び出し、新しく更新されたエンティティnewItemを渡します。完成したメソッドは次のようになります。
fun sellItem(item: Item) {
if (item.quantityInStock > 0) {
// Decrease the quantity by 1
val newItem = item.copy(quantityInStock = item.quantityInStock - 1)
updateItem(newItem)
}
}
- 在庫販売機能を追加するために、
ItemDetailFragmentに移動します。bind()関数の最後までスクロールします。applyブロック内で、[SELL] ボタンにクリック リスナーを設定し、viewModelに対してsellItem()関数を呼び出します。
private fun bind(item: Item) {
binding.apply {
...
sellItem.setOnClickListener { viewModel.sellItem(item) }
}
}
- アプリを実行します。[Inventory] 画面で、数量がゼロより大きいリスト要素をクリックします。[Item Details] 画面が表示されます。[SELL] ボタンをタップすると、数量の値が 1 減ります。

- [Item Details] 画面で、[SELL] ボタンをタップし続けて数量を 0 にします(ヒント: 在庫が少ないエンティティを選択するか、数量の少ないエンティティを新規作成します)。数量がゼロになったら、[SELL] ボタンをタップします。表示は変化しません。これは、数量がゼロより大きいかどうかが
sellItem()関数によって確認されてから、数量が更新されるためです。

- 適切なフィードバックをユーザーに提供するために、販売するアイテムがないときは [SELL] ボタンを無効にすることをおすすめします。
InventoryViewModelで、数量が0より大きいかどうかを確認する関数を追加します。Itemインスタンスを受け取ってBooleanを返す関数isStockAvailable()に名前を付けます。
fun isStockAvailable(item: Item): Boolean {
return (item.quantityInStock > 0)
}
ItemDetailFragmentに移動して、bind()関数までスクロールします。apply ブロック内で、viewModelに対してisStockAvailable()関数を呼び出し、itemを渡します。[SELL] ボタンのisEnabledプロパティに戻り値を設定します。コードは次のようになります。
private fun bind(item: Item) {
binding.apply {
...
sellItem.isEnabled = viewModel.isStockAvailable(item)
sellItem.setOnClickListener { viewModel.sellItem(item) }
}
}
- アプリを実行します。在庫の数量がゼロになると [SELL] ボタンが無効になります。これで、アイテム販売機能がアプリに実装されました。

アイテム エンティティを削除する
前のタスクと同様に、削除関数を実装することでアプリの機能をさらに拡張します。このステップの大まかな流れは次のとおりです。販売機能を実装する場合よりもはるかに簡単です。
- データベースからエンティティを削除する関数を ViewModel に追加します。
ItemDetailFragmentに新しいメソッドを追加して、新しい削除関数を呼び出し、ナビゲーションを処理するようにします。- [DELETE] ボタンにクリック リスナーを追加します。
コーディングに進みましょう。
InventoryViewModelに、deleteItem()という新しい関数を追加します。これはitemというItemエンティティ クラスのインスタンスを受け取り、何も返しません。deleteItem()関数内で、viewModelScopeを使用してコルーチンを起動します。launchブロック内で、itemDaoに対してdelete()メソッドを呼び出し、itemを渡します。
fun deleteItem(item: Item) {
viewModelScope.launch {
itemDao.delete(item)
}
}
ItemDetailFragmentで、deleteItem()関数の先頭までスクロールします。viewModelに対してdeleteItem()を呼び出し、itemを渡します。itemインスタンスには、[Item Details] 画面に現在表示されているエンティティが含まれています。完成したメソッドは次のようになります。
private fun deleteItem() {
viewModel.deleteItem(item)
findNavController().navigateUp()
}
- 引き続き
ItemDetailFragment内で、showConfirmationDialog()関数までスクロールします。この関数は、スターター コードの一部として提供されています。このメソッドは、アイテムを削除する前にユーザーの確認を得るためのアラート ダイアログを表示し、肯定的なボタンがタップされるとdeleteItem()関数を呼び出します。
private fun showConfirmationDialog() {
MaterialAlertDialogBuilder(requireContext())
...
.setPositiveButton(getString(R.string.yes)) { _, _ ->
deleteItem()
}
.show()
}
showConfirmationDialog() 関数は、次のようなアラート ダイアログを表示します。

ItemDetailFragmentで、bind()関数の最後、applyブロック内で、削除ボタンにクリック リスナーを設定します。クリック リスナー ラムダ内でshowConfirmationDialog()を呼び出します。
private fun bind(item: Item) {
binding.apply {
...
deleteItem.setOnClickListener { showConfirmationDialog() }
}
}
- アプリを実行します。インベントリ リスト画面でリスト要素を選択し、[Item Details] 画面で [DELETE] ボタンをタップします。[YES] をタップします。アプリが [Inventory] 画面に戻ります。削除したエンティティが、アプリ データベースに存在しなくなっています。これで削除機能が実装されました。

アイテム エンティティを編集する
前のタスクと同様に、このタスクではアプリにもうひとつ機能を追加します。アイテム エンティティの編集機能を実装します。
アプリ データベースのエンティティを編集する手順を簡単に示すと、次のようになります。
- フラグメントのタイトルを「Edit Item」に更新して [Add Item] 画面を再利用します。
- FAB にクリック リスナーを追加し、[Edit Item] 画面に移動します。
- TextView にエンティティの詳細を入力します。
- Room を使用してデータベースのエンティティを更新します。
FAB にクリック リスナーを追加する
ItemDetailFragmentに、editItem()という新しいprivate関数を追加します。これはパラメータを受け取らず、何も返しません。次のステップでは、画面のタイトルを [Edit Item] に更新してfragment_add_item.xmlを再利用します。そのために、アクションの一部としてアイテム ID とともにフラグメントのタイトル文字列を送信します。
private fun editItem() {
}
フラグメントのタイトルを更新すると、[Edit Item] 画面は次のようになります。

editItem()関数内で、actionという不変変数を作成します。ItemDetailFragmentDirectionsに対してactionItemDetailFragmentToAddItemFragment()を呼び出し、タイトル文字列edit_fragment_titleとアイテムidを渡します。戻り値をactionに割り当てます。actionの定義の下で、this.findNavController().navigate()を呼び出してactionを渡し、[Edit Item] 画面に移動します。
private fun editItem() {
val action = ItemDetailFragmentDirections.actionItemDetailFragmentToAddItemFragment(
getString(R.string.edit_fragment_title),
item.id
)
this.findNavController().navigate(action)
}
- 引き続き
ItemDetailFragment内で、bind()関数までスクロールします。applyブロック内で、FAB にクリック リスナーを設定し、ラムダからeditItem()関数を呼び出して [Edit Item] 画面に移動します。
private fun bind(item: Item) {
binding.apply {
...
editItem.setOnClickListener { editItem() }
}
}
- アプリを実行します。[Item Details] 画面に移動します。FAB をクリックします。画面のタイトルが [Edit Item] に更新されていますが、すべてのテキスト フィールドは空白になっています。これは次のステップで修正します。

TextView にデータを入力する
このステップでは、[Edit Item] 画面のテキスト フィールドにエンティティの詳細を入力します。Add Item 画面を使用しているため、Kotlin ファイル AddItemFragment.kt に新しい関数を追加します。
AddItemFragmentで、新しいprivate関数を追加してテキスト フィールドをエンティティの詳細にバインドします。Item エンティティ クラスのインスタンスを受け取って何も返さない関数bind()に名前を付けます。
private fun bind(item: Item) {
}
bind()関数の実装は、前にItemDetailFragmentで行ったことと非常によく似ています。bind()関数内で、次のようにformat()関数を使用して価格を小数第 2 位に丸め、priceというvalに割り当てます。
val price = "%.2f".format(item.itemPrice)
price定義の下で、次のようにbindingプロパティでapplyスコープ関数を使用します。
binding.apply {
}
applyスコープ関数のコードブロック内で、itemNameの text プロパティにitem.itemNameを設定します。setText()関数を使用し、item.itemName文字列と、BufferTypeとしてTextView.BufferType.SPANNABLEを渡します。
binding.apply {
itemName.setText(item.itemName, TextView.BufferType.SPANNABLE)
}
Android Studio のプロンプトが表示されたら、android.widget.TextView をインポートします。
- 上のステップと同様に、次のように価格
EditTextの text プロパティを設定します。数量 EditText のtextプロパティを設定する場合は、item.quantityInStockをStringに変換する必要があります。完成した関数は次のようになります。
private fun bind(item: Item) {
val price = "%.2f".format(item.itemPrice)
binding.apply {
itemName.setText(item.itemName, TextView.BufferType.SPANNABLE)
itemPrice.setText(price, TextView.BufferType.SPANNABLE)
itemCount.setText(item.quantityInStock.toString(), TextView.BufferType.SPANNABLE)
}
}
- 引き続き
AddItemFragment内で、onViewCreated()関数までスクロールします。スーパークラス関数の呼び出しの後、idというvalを作成し、ナビゲーション引数からitemIdを取得します。
val id = navigationArgs.itemId
idがゼロより大きいかどうかを確認する条件を指定してif-elseブロックを追加し、[SAVE] ボタンのクリック リスナーをelseブロックに移動します。ifブロック内で、idを使用してエンティティを取得し、オブザーバーを追加します。オブザーバー内で、itemプロパティを更新し、bind()を呼び出してitemを渡します。コピーして貼り付けるために、完成した関数を次に示します。シンプルでわかりやすくなっていますので、あとはご自身で解読してみてください。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val id = navigationArgs.itemId
if (id > 0) {
viewModel.retrieveItem(id).observe(this.viewLifecycleOwner) { selectedItem ->
item = selectedItem
bind(item)
}
} else {
binding.saveAction.setOnClickListener {
addNewItem()
}
}
}
- アプリを実行します。[Item Details] に移動して、[+] FAB をタップします。フィールドにアイテムの詳細が入力されています。在庫の数量または他のフィールドを編集し、[SAVE] ボタンをタップします。何も起こりません。アプリ データベースのエンティティを更新していないためです。これは後ほどすぐに修正します。

Room を使用してエンティティを更新する
この最後のタスクでは、コードの最後の要素を追加して更新機能を実装します。ViewModel で必要な関数を定義し、AddItemFragment で使用します。
それではまたコーディングしてみましょう。
InventoryViewModelに、Intおよびエンティティの詳細の文字列 3 つ(itemName、itemPrice、itemCount)を受け取るgetUpdatedItemEntry()というprivate関数を追加します。関数からItemのインスタンスを返します。参考までに、コードを示します。
private fun getUpdatedItemEntry(
itemId: Int,
itemName: String,
itemPrice: String,
itemCount: String
): Item {
}
getUpdatedItemEntry()関数内で、次のように関数パラメータを使用して Item インスタンスを作成します。関数からItemインスタンスを返します。
private fun getUpdatedItemEntry(
itemId: Int,
itemName: String,
itemPrice: String,
itemCount: String
): Item {
return Item(
id = itemId,
itemName = itemName,
itemPrice = itemPrice.toDouble(),
quantityInStock = itemCount.toInt()
)
}
- 引き続き
InventoryViewModel内で、updateItem()という別の関数を追加します。この関数もIntとエンティティの詳細の文字列 3 つを受け取り、何も返しません。次のコード スニペットの変数名を使用します。
fun updateItem(
itemId: Int,
itemName: String,
itemPrice: String,
itemCount: String
) {
}
updateItem()関数内で、次のようにgetUpdatedItemEntry()関数を呼び出し、関数パラメータとしてエンティティ情報を渡します。戻り値を、updatedItemという不変変数に割り当てます。
val updatedItem = getUpdatedItemEntry(itemId, itemName, itemPrice, itemCount)
getUpdatedItemEntry()関数の呼び出しのすぐ下で、updateItem()関数を呼び出してupdatedItemを渡します。完成した関数は次のようになります。
fun updateItem(
itemId: Int,
itemName: String,
itemPrice: String,
itemCount: String
) {
val updatedItem = getUpdatedItemEntry(itemId, itemName, itemPrice, itemCount)
updateItem(updatedItem)
}
AddItemFragmentに戻り、updateItem()というプライベート関数を追加します。これはパラメータを受け取らず、何も返しません。関数内で、if条件を追加し、isEntryValid()関数を呼び出してユーザー入力を検証します。
private fun updateItem() {
if (isEntryValid()) {
}
}
ifブロック内で、viewModel.updateItem()を呼び出してエンティティの詳細を渡します。次のように、ナビゲーション引数のitemIdと、EditText の名前、価格、数量など、その他のエンティティの詳細を使用します。
viewModel.updateItem(
this.navigationArgs.itemId,
this.binding.itemName.text.toString(),
this.binding.itemPrice.text.toString(),
this.binding.itemCount.text.toString()
)
updateItem()関数呼び出しの下で、actionというvalを定義します。AddItemFragmentDirectionsに対してactionAddItemFragmentToItemListFragment()を呼び出し、戻り値をactionに割り当てます。ItemListFragmentに移動し、findNavController().navigate()を呼び出してactionを渡します。
private fun updateItem() {
if (isEntryValid()) {
viewModel.updateItem(
this.navigationArgs.itemId,
this.binding.itemName.text.toString(),
this.binding.itemPrice.text.toString(),
this.binding.itemCount.text.toString()
)
val action = AddItemFragmentDirections.actionAddItemFragmentToItemListFragment()
findNavController().navigate(action)
}
}
- 引き続き
AddItemFragment内で、bind()関数までスクロールします。binding.applyスコープ関数ブロック内で、[SAVE] ボタンのクリック リスナーを設定します。次のように、ラムダ内でupdateItem()関数を呼び出します。
private fun bind(item: Item) {
...
binding.apply {
...
saveAction.setOnClickListener { updateItem() }
}
}
- アプリを実行します。インベントリ アイテムを編集してみましょう。Inventory アプリ データベースのアイテムはいずれも編集できるはずです。

Room を使用してアプリ データベースを管理する初めてのアプリを作成しました。
6. 解答コード
この Codelab の解答コードは、以下に示す GitHub リポジトリとブランチにあります。
この Codelab のコードを取得して Android Studio で開くには、以下の手順に沿って操作します。
コードを取得する
- 指定された URL をクリックします。プロジェクトの GitHub ページがブラウザで開きます。
- プロジェクトの GitHub ページで、[Code] ボタンをクリックすると、ダイアログが表示されます。

- ダイアログで、[Download ZIP] をクリックして、プロジェクトをパソコンに保存します。ダウンロードが完了するまで待ってください。
- パソコンに保存したファイルを見つけます([ダウンロード] フォルダなど)。
- ZIP ファイルをダブルクリックして展開します。プロジェクト ファイルが入った新しいフォルダが作成されます。
Android Studio でプロジェクトを開く
- Android Studio を起動します。
- [Welcome to Android Studio] ウィンドウで [Open an existing Android Studio project] をクリックします。

注: Android Studio がすでに開いている場合は、メニューから [File] > [New] > [Import Project] を選択します。

- [Import Project] ダイアログで、展開したプロジェクト フォルダがある場所([ダウンロード] フォルダなど)に移動します。
- そのプロジェクト フォルダをダブルクリックします。
- Android Studio でプロジェクトが開くまで待ちます。
- 実行ボタン
をクリックして、アプリをビルドし、実行します。期待どおりに動作することを確認します。 - [Project] ツール ウィンドウでプロジェクト ファイルを見て、アプリがどのように実装されているかを確認します。
7. まとめ
- Kotlin には、クラスを継承する、またはクラスの既存の定義を変更することなく、新しい関数をクラスに追加する機能が用意されています。これは「拡張機能」という特別な宣言を介して行います。
FlowデータをLiveData値として使用するために、asLiveData()関数を使用します。copy()関数は、データクラスのすべてのインスタンスにデフォルトで提供されます。オブジェクトをコピーして一部のプロパティを変更し、残りのプロパティは変更しないでおくことができます。
8. 詳細
Android デベロッパー ドキュメント
- デスティネーション間でデータを渡す
- Android String
- Android Formatter
- Database Inspector を使用してデータベースをデバッグする
- Room を使用してローカル データベースにデータを保存する
API リファレンス
Kotlin リファレンス

