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
クラスに実装します。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 を使用する
このタスクでは 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
で行ったことと同様です。itemName
TextView のtext
プロパティをitem.itemName
に設定します。item
プロパティに対してgetFormattedPrice
()
を呼び出して価格値を書式設定し、itemPrice
TextView のtext
プロパティに設定します。quantityInStock
をString
に変換し、itemQuantity
TextView の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 リファレンス