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 リファレンス
 
  
