Room によるデータの読み取りと更新

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] 画面を使用してアプリ データベースにデータを追加できます。

注: 現在のバージョンのスターター アプリでは、データベースに保存されている日付は表示されません。

771c6a677ecd96c7.png

この Codelab ではアプリを拡張し、Room ライブラリを使用して、データの読み取りと表示、データベースのエンティティの更新と削除を行えるようにします。

この Codelab のスターター コードをダウンロードする

このスターター コードは前の Codelab の解答コードと同じものです。

この Codelab のコードを取得して Android Studio で開くには、以下の手順に沿って操作します。

コードを取得する

  1. 指定された URL をクリックします。プロジェクトの GitHub ページがブラウザで開きます。
  2. プロジェクトの GitHub ページで、[Code] ボタンをクリックすると、ダイアログが表示されます。

5b0a76c50478a73f.png

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

Android Studio でプロジェクトを開く

  1. Android Studio を起動します。
  2. [Welcome to Android Studio] ウィンドウで [Open an existing Android Studio project] をクリックします。

36cc44fcf0f89a1d.png

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

21f3eec988dcfbe9.png

  1. [Import Project] ダイアログで、展開したプロジェクト フォルダがある場所([ダウンロード] フォルダなど)に移動します。
  2. そのプロジェクト フォルダをダブルクリックします。
  3. Android Studio でプロジェクトが開くまで待ちます。
  4. 実行ボタン 11c34fc5e516fb1c.png をクリックして、アプリをビルドし、実行します。期待どおりに動作することを確認します。
  5. [Project] ツール ウィンドウでプロジェクト ファイルを見て、アプリがどのように実装されているかを確認します。

3.RecyclerView を追加する

このタスクではアプリに RecyclerView を追加して、データベースに格納されているデータを表示します。

ヘルパー関数を追加して価格を書式設定する

最終的なアプリのスクリーンショットを次に示します。

d6e7b7b9f12e7a16.png

価格が通貨形式で表示されています。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")
}

このステップでは、アイテムの価格を通貨形式の文字列に書式設定します。一般に、データを書式設定するだけのためにデータを表すエンティティ クラスを変更することはせず(単一責任の原則を参照)、拡張関数を追加します。

  1. Item.kt で、クラス定義の下に、パラメータを受け取らずに String を返す Item.getFormattedPrice() という拡張関数を追加します。クラス名と、関数名のドット表記に注目してください。
fun Item.getFormattedPrice(): String =
   NumberFormat.getCurrencyInstance().format(itemPrice)

Android Studio のプロンプトが表示されたら、java.text.NumberFormat をインポートします。

ListAdapter を追加する

このステップでは、RecyclerView にリスト アダプターを追加します。リスト アダプターの実装については以前の Codelab で説明済みのため、手順の概要のみを次に示します。参考までに、完成した ItemListAdapter ファイルをこのステップの最後に掲載しています。この Codelab での Room のコンセプトについて理解を深めるためにご利用ください。

  1. com.example.inventory パッケージで、ItemListAdapter という Kotlin クラスを追加します。コンストラクタ パラメータとして onItemClicked() という関数を渡します。これはパラメータとして Item オブジェクトを受け取ります。
  2. ListAdapter を拡張するように、ItemListAdapter クラスのシグネチャを変更します。ItemItemListAdapter.ItemViewHolder をパラメータとして渡します。
  3. コンストラクタ パラメータ DiffCallback を追加します。ListAdapter は、これを使用してリスト内の変更点を把握します。
  4. 必要なメソッド onCreateViewHolder()onBindViewHolder() をオーバーライドします。
  5. RecyclerView が必要とする場合、onCreateViewHolder() メソッドは新しい ViewHolder を返します。
  6. onCreateViewHolder() メソッド内で新しい View を作成し、自動生成されたバインディング クラス ItemListItemBinding を使用して item_list_item.xml レイアウト ファイルからインフレートします。
  7. onBindViewHolder() メソッドを実装します。getItem() メソッドを使用し、位置を渡して現在のアイテムを取得します。
  8. itemView にクリック リスナーを設定し、リスナー内で関数 onItemClicked() を呼び出します。
  9. ItemViewHolder クラスを定義し、RecyclerView.ViewHolder. から拡張します。bind() 関数をオーバーライドし、Item オブジェクトを渡します。
  10. コンパニオン オブジェクトを定義します。コンパニオン オブジェクト内で、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 にエンティティの詳細をバインドします。

9c416f2fbf1e5ae2.png

  1. ItemListAdapter.kt で、bind() 関数を ItemViewHolder クラスに実装します。itemName TextView を item.itemName にバインドします。getFormattedPrice() 拡張関数を使用して通貨形式の価格を取得し、itemPrice TextView にバインドします。quantityInStock 値を String に変換し、itemQuantity TextView にバインドします。完成したメソッドは次のようになります。
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 を使用する

このタスクでは InventoryViewModelItemListFragment を更新し、前のステップで作成したリスト アダプターを使用してアイテムの詳細を画面に表示します。

  1. InventoryViewModel クラスの先頭で、データベースのアイテムについて、LiveData<List<Item>> 型の allItems という val を作成します。エラーはこの後すぐに修正するため、気にする必要はありません。
val allItems: LiveData<List<Item>>

Android Studio のプロンプトが表示されたら、androidx.lifecycle.LiveData をインポートします。

  1. itemDao に対して getItems() を呼び出し、allItems に割り当てます。getItems() 関数は Flow を返します。データを LiveData 値として使用するために、asLiveData() 関数を使用します。完成した定義は次のようになります。
val allItems: LiveData<List<Item>> = itemDao.getItems().asLiveData()

Android Studio のプロンプトが表示されたら、androidx.lifecycle.asLiveData をインポートします。

  1. 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 をインポートします。

  1. 引き続き ItemListFragment 内で、関数 onViewCreated() までスクロールします。super.onViewCreated() 呼び出しの下で、adapter という val を宣言します。何も渡さずにデフォルトのコンストラクタ ItemListAdapter{} を使用して、新しい adapter プロパティを初期化します。
  2. 次のように、新しく作成した adapterrecyclerView にバインドします。
val adapter = ItemListAdapter {
}
binding.recyclerView.adapter = adapter
  1. 引き続き onViewCreated() 内で、アダプターを設定した後、オブザーバーを allItems にアタッチして、データの変更をリッスンします。
  2. オブザーバー内で、adapter に対して submitList() を呼び出し、新しいリストを渡します。これにより RecyclerView が、リストに含まれる新しいアイテムで更新されます。
viewModel.allItems.observe(this.viewLifecycleOwner) { items ->
   items.let {
       adapter.submitList(it)
   }
}
  1. 完成した 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)
   }
}

9c416f2fbf1e5ae2.png

4. アイテムの詳細を表示する

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

d699618f5d9437df.png

このタスクでは次の手順を実施します。

  • RecyclerView にクリック ハンドラを追加して、アプリを [Item Details] 画面に移動する。
  • ItemListFragment フラグメントで、データベースからデータを取得して表示する。
  • TextView を ViewModel データにバインドする。

クリック ハンドラを追加する

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

196553111ee69beb.png

後のステップで、エンティティの詳細を [Item Details] 画面に表示し、[SELL] ボタンや [DELETE] ボタンに機能を追加します。

アイテムの詳細を取得する

このステップでは、InventoryViewModel に新しい関数を追加して、アイテム id に基づいてデータベースからアイテムの詳細を取得します。次のステップでは、この関数を使用してエンティティの詳細を [Item Details] 画面に表示します。

  1. InventoryViewModel で、アイテム ID に Int を受け取って LiveData<Item> を返す、retrieveItem() という関数を追加します。戻り値エラーはこの後すぐに修正します。
fun retrieveItem(id: Int): LiveData<Item> {
}
  1. 新しい関数内で、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 のデータにオブザーバーをアタッチして、基になるデータベースのデータが変更された場合に画面上のインベントリ リストが更新されるようにします。

  1. ItemDetailFragment で、Item エンティティ型の item という可変プロパティを追加します。このプロパティは、1 つのエンティティに関する情報を格納するために使用します。このプロパティは後で初期化されるため、先頭に lateinit を付けます。
lateinit var item: Item

Android Studio のプロンプトが表示されたら、com.example.inventory.data.Item をインポートします。

  1. 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 をインポートします。

  1. 引き続き ItemDetailFragment で、bind() という private 関数を作成します。これは Item エンティティのインスタンスをパラメータとして受け取り、何も返しません。
private fun bind(item: Item) {
}
  1. bind() 関数を実装します。これは、ItemListAdapter で行ったことと同様です。itemName TextView の text プロパティを item.itemName に設定します。item プロパティに対して getFormattedPrice() を呼び出して価格値を書式設定し、itemPrice TextView の text プロパティに設定します。quantityInStockString に変換し、itemQuantity TextView の text プロパティに設定します。
private fun bind(item: Item) {
   binding.itemName.text = item.itemName
   binding.itemPrice.text = item.getFormattedPrice()
   binding.itemCount.text = item.quantityInStock.toString()
}
  1. 次に示すように、コードブロックに apply{} スコープ関数を使用するように bind() 関数を更新します。
private fun bind(item: Item) {
   binding.apply {
       itemName.text = item.itemName
       itemPrice.text = item.getFormattedPrice()
       itemCount.text = item.quantityInStock.toString()
   }
}
  1. 引き続き ItemDetailFragment で、onViewCreated() をオーバーライドします。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   super.onViewCreated(view, savedInstanceState)
}
  1. これまでのステップで、ItemListFragment から ItemDetailFragment にナビゲーション引数としてアイテム ID を渡しました。onViewCreated() 内で、スーパークラス関数の呼び出しの下に id という不変変数を作成します。ナビゲーション引数を取得してこの新しい変数に割り当てます。
val id = navigationArgs.itemId
  1. 次に、この id 変数を使用してアイテムの詳細を取得します。onViewCreated() 内で、viewModel に対して retrieveItem() 関数を呼び出し、id を渡します。viewLifecycleOwner とラムダを渡して、戻り値にオブザーバーをアタッチします。
viewModel.retrieveItem(id).observe(this.viewLifecycleOwner) {
   }
  1. ラムダ内で、データベースから取得した 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)
   }
}
  1. アプリを実行します。[Inventory] 画面でリスト要素をクリックすると、[Item Details] 画面が表示されます。今度は空白画面ではなく、インベントリ データベースから取得したエンティティの詳細が表示されています。

  1. [SELL] ボタン、[DELETE] ボタン、FAB をタップします。何も起こりません。これらのボタンの機能は次のタスクで実装します。

5. アイテム販売機能を実装する

このタスクでは、アプリの機能を拡張し、販売機能を実装します。このステップの大まかな流れは次のとおりです。

  • ViewModel に関数を追加して、エンティティを更新できるようにする。
  • 新しいメソッドを作成し、数量を減らしてアプリ データベースのエンティティを更新できるようにする。
  • [SELL] ボタンにクリック リスナーを追加する。
  • 数量がゼロの場合は [SELL] ボタンを無効にする。

コーディングしましょう。

  1. InventoryViewModel で、updateItem() というプライベート関数を追加します。これはエンティティ クラス Item のインスタンスを受け取り、何も返しません。
private fun updateItem(item: Item) {
}
  1. 新しいメソッド updateItem() を実装します。ItemDao クラスから update() サスペンド メソッドを呼び出すには、viewModelScope を使用してコルーチンを起動します。起動ブロック内で、itemDao に対して update() 関数を呼び出し、item を渡します。完成したメソッドは、次のようになります。
private fun updateItem(item: Item) {
   viewModelScope.launch {
       itemDao.update(item)
   }
}
  1. 引き続き InventoryViewModel 内で、sellItem() という別のメソッドを追加します。これは Item エンティティ クラスのインスタンスを受け取り、何も返しません。
fun sellItem(item: Item) {
}
  1. sellItem() 関数内で、if 条件を追加して item.quantityInStock0 より大きいかどうかを確認します。
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)
  1. InventoryViewModelsellItem() 関数に戻ります。if ブロック内で、newItem という新しい不変プロパティを作成します。item インスタンスに対して copy() 関数を呼び出し、更新された quantityInStock を渡します。これにより、在庫が 1 減少します。
val newItem = item.copy(quantityInStock = item.quantityInStock - 1)
  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)
   }
}
  1. 在庫販売機能を追加するために、ItemDetailFragment に移動します。bind() 関数の最後までスクロールします。apply ブロック内で、[SELL] ボタンにクリック リスナーを設定し、viewModel に対して sellItem() 関数を呼び出します。
private fun bind(item: Item) {
binding.apply {

...
    sellItem.setOnClickListener { viewModel.sellItem(item) }
    }
}
  1. アプリを実行します。[Inventory] 画面で、数量がゼロより大きいリスト要素をクリックします。[Item Details] 画面が表示されます。[SELL] ボタンをタップすると、数量の値が 1 減ります。

aa63ca761dc8f009.png

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

3e099d3c55596938.png

  1. 適切なフィードバックをユーザーに提供するために、販売するアイテムがないときは [SELL] ボタンを無効にすることをおすすめします。InventoryViewModel で、数量が 0 より大きいかどうかを確認する関数を追加します。Item インスタンスを受け取って Boolean を返す関数 isStockAvailable() に名前を付けます。
fun isStockAvailable(item: Item): Boolean {
   return (item.quantityInStock > 0)
}
  1. 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) }
   }
}
  1. アプリを実行します。在庫の数量がゼロになると [SELL] ボタンが無効になります。これで、アイテム販売機能がアプリに実装されました。

5e49db8451e77c2b.png

アイテム エンティティを削除する

前のタスクと同様に、削除関数を実装することでアプリの機能をさらに拡張します。このステップの大まかな流れは次のとおりです。販売機能を実装する場合よりもはるかに簡単です。

  • データベースからエンティティを削除する関数を ViewModel に追加します。
  • ItemDetailFragment に新しいメソッドを追加して、新しい削除関数を呼び出し、ナビゲーションを処理するようにします。
  • [DELETE] ボタンにクリック リスナーを追加します。

コーディングに進みましょう。

  1. InventoryViewModel に、deleteItem() という新しい関数を追加します。これは item という Item エンティティ クラスのインスタンスを受け取り、何も返しません。deleteItem() 関数内で、viewModelScope を使用してコルーチンを起動します。launch ブロック内で、itemDao に対して delete() メソッドを呼び出し、item を渡します。
fun deleteItem(item: Item) {
   viewModelScope.launch {
       itemDao.delete(item)
   }
}
  1. ItemDetailFragment で、deleteItem() 関数の先頭までスクロールします。viewModel に対して deleteItem() を呼び出し、item を渡します。item インスタンスには、[Item Details] 画面に現在表示されているエンティティが含まれています。完成したメソッドは次のようになります。
private fun deleteItem() {
   viewModel.deleteItem(item)
   findNavController().navigateUp()
}
  1. 引き続き ItemDetailFragment 内で、showConfirmationDialog() 関数までスクロールします。この関数は、スターター コードの一部として提供されています。このメソッドは、アイテムを削除する前にユーザーの確認を得るためのアラート ダイアログを表示し、肯定的なボタンがタップされると deleteItem() 関数を呼び出します。
private fun showConfirmationDialog() {
        MaterialAlertDialogBuilder(requireContext())
            ...
            .setPositiveButton(getString(R.string.yes)) { _, _ ->
                deleteItem()
            }
            .show()
    }

showConfirmationDialog() 関数は、次のようなアラート ダイアログを表示します。

728bfcbb997c8017.png

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

c05318ab8c216fa1.png

アイテム エンティティを編集する

前のタスクと同様に、このタスクではアプリにもうひとつ機能を追加します。アイテム エンティティの編集機能を実装します。

アプリ データベースのエンティティを編集する手順を簡単に示すと、次のようになります。

  • フラグメントのタイトルを「Edit Item」に更新して [Add Item] 画面を再利用します。
  • FAB にクリック リスナーを追加し、[Edit Item] 画面に移動します。
  • TextView にエンティティの詳細を入力します。
  • Room を使用してデータベースのエンティティを更新します。

FAB にクリック リスナーを追加する

  1. ItemDetailFragment に、editItem() という新しい private 関数を追加します。これはパラメータを受け取らず、何も返しません。次のステップでは、画面のタイトルを [Edit Item] に更新して fragment_add_item.xml を再利用します。そのために、アクションの一部としてアイテム ID とともにフラグメントのタイトル文字列を送信します。
private fun editItem() {
}

フラグメントのタイトルを更新すると、[Edit Item] 画面は次のようになります。

bcd407af7c515a21.png

  1. 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)
}
  1. 引き続き ItemDetailFragment 内で、bind() 関数までスクロールします。apply ブロック内で、FAB にクリック リスナーを設定し、ラムダから editItem() 関数を呼び出して [Edit Item] 画面に移動します。
private fun bind(item: Item) {
   binding.apply {
       ...
       editItem.setOnClickListener { editItem() }
   }
}
  1. アプリを実行します。[Item Details] 画面に移動します。FAB をクリックします。画面のタイトルが [Edit Item] に更新されていますが、すべてのテキスト フィールドは空白になっています。これは次のステップで修正します。

a6a6583171b68230.png

TextView にデータを入力する

このステップでは、[Edit Item] 画面のテキスト フィールドにエンティティの詳細を入力します。Add Item 画面を使用しているため、Kotlin ファイル AddItemFragment.kt に新しい関数を追加します。

  1. AddItemFragment で、新しい private 関数を追加してテキスト フィールドをエンティティの詳細にバインドします。Item エンティティ クラスのインスタンスを受け取って何も返さない関数 bind() に名前を付けます。
private fun bind(item: Item) {
}
  1. bind() 関数の実装は、前に ItemDetailFragment で行ったことと非常によく似ています。bind() 関数内で、次のように format() 関数を使用して価格を小数第 2 位に丸め、price という val に割り当てます。
val price = "%.2f".format(item.itemPrice)
  1. price 定義の下で、次のように binding プロパティで apply スコープ関数を使用します。
binding.apply {
}
  1. 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 をインポートします。

  1. 上のステップと同様に、次のように価格 EditText の text プロパティを設定します。数量 EditText の text プロパティを設定する場合は、item.quantityInStockString に変換する必要があります。完成した関数は次のようになります。
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)
   }
}
  1. 引き続き AddItemFragment 内で、onViewCreated() 関数までスクロールします。スーパークラス関数の呼び出しの後、id という val を作成し、ナビゲーション引数から itemId を取得します。
val id = navigationArgs.itemId
  1. 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()
       }
   }
}
  1. アプリを実行します。[Item Details] に移動して、[+] FAB をタップします。フィールドにアイテムの詳細が入力されています。在庫の数量または他のフィールドを編集し、[SAVE] ボタンをタップします。何も起こりません。アプリ データベースのエンティティを更新していないためです。これは後ほどすぐに修正します。

829ceb9dd7993215.png

Room を使用してエンティティを更新する

この最後のタスクでは、コードの最後の要素を追加して更新機能を実装します。ViewModel で必要な関数を定義し、AddItemFragment で使用します。

それではまたコーディングしてみましょう。

  1. InventoryViewModel に、Int およびエンティティの詳細の文字列 3 つ(itemNameitemPriceitemCount)を受け取る getUpdatedItemEntry() という private 関数を追加します。関数から Item のインスタンスを返します。参考までに、コードを示します。
private fun getUpdatedItemEntry(
   itemId: Int,
   itemName: String,
   itemPrice: String,
   itemCount: String
): Item {
}
  1. 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()
   )
}
  1. 引き続き InventoryViewModel 内で、updateItem() という別の関数を追加します。この関数も Int とエンティティの詳細の文字列 3 つを受け取り、何も返しません。次のコード スニペットの変数名を使用します。
fun updateItem(
   itemId: Int,
   itemName: String,
   itemPrice: String,
   itemCount: String
) {
}
  1. updateItem() 関数内で、次のように getUpdatedItemEntry() 関数を呼び出し、関数パラメータとしてエンティティ情報を渡します。戻り値を、updatedItem という不変変数に割り当てます。
val updatedItem = getUpdatedItemEntry(itemId, itemName, itemPrice, itemCount)
  1. getUpdatedItemEntry() 関数の呼び出しのすぐ下で、updateItem() 関数を呼び出して updatedItem を渡します。完成した関数は次のようになります。
fun updateItem(
   itemId: Int,
   itemName: String,
   itemPrice: String,
   itemCount: String
) {
   val updatedItem = getUpdatedItemEntry(itemId, itemName, itemPrice, itemCount)
   updateItem(updatedItem)
}
  1. AddItemFragment に戻り、updateItem() というプライベート関数を追加します。これはパラメータを受け取らず、何も返しません。関数内で、if 条件を追加し、isEntryValid() 関数を呼び出してユーザー入力を検証します。
private fun updateItem() {
   if (isEntryValid()) {
   }
}
  1. 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()
)
  1. 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)
   }
}
  1. 引き続き AddItemFragment 内で、bind() 関数までスクロールします。binding.apply スコープ関数ブロック内で、[SAVE] ボタンのクリック リスナーを設定します。次のように、ラムダ内で updateItem() 関数を呼び出します。
private fun bind(item: Item) {
   ...
   binding.apply {
       ...
       saveAction.setOnClickListener { updateItem() }
   }
}
  1. アプリを実行します。インベントリ アイテムを編集してみましょう。Inventory アプリ データベースのアイテムはいずれも編集できるはずです。

1bbd094a77c25fc4.png

Room を使用してアプリ データベースを管理する初めてのアプリを作成しました。

6. 解答コード

この Codelab の解答コードは、以下に示す GitHub リポジトリとブランチにあります。

この Codelab のコードを取得して Android Studio で開くには、以下の手順に沿って操作します。

コードを取得する

  1. 指定された URL をクリックします。プロジェクトの GitHub ページがブラウザで開きます。
  2. プロジェクトの GitHub ページで、[Code] ボタンをクリックすると、ダイアログが表示されます。

5b0a76c50478a73f.png

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

Android Studio でプロジェクトを開く

  1. Android Studio を起動します。
  2. [Welcome to Android Studio] ウィンドウで [Open an existing Android Studio project] をクリックします。

36cc44fcf0f89a1d.png

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

21f3eec988dcfbe9.png

  1. [Import Project] ダイアログで、展開したプロジェクト フォルダがある場所([ダウンロード] フォルダなど)に移動します。
  2. そのプロジェクト フォルダをダブルクリックします。
  3. Android Studio でプロジェクトが開くまで待ちます。
  4. 実行ボタン 11c34fc5e516fb1c.png をクリックして、アプリをビルドし、実行します。期待どおりに動作することを確認します。
  5. [Project] ツール ウィンドウでプロジェクト ファイルを見て、アプリがどのように実装されているかを確認します。

7. まとめ

  • Kotlin には、クラスを継承する、またはクラスの既存の定義を変更することなく、新しい関数をクラスに追加する機能が用意されています。これは「拡張機能」という特別な宣言を介して行います。
  • Flow データを LiveData 値として使用するために、asLiveData() 関数を使用します。
  • copy() 関数は、データクラスのすべてのインスタンスにデフォルトで提供されます。オブジェクトをコピーして一部のプロパティを変更し、残りのプロパティは変更しないでおくことができます。

8. 詳細

Android デベロッパー ドキュメント

API リファレンス

Kotlin リファレンス