1. 始める前に
前の Codelab では、Room 永続ライブラリ(SQLite データベース上の抽象化レイヤ)を使用してアプリデータを保存する方法を学習しました。この Codelab では、Inventory アプリに機能を追加し、Room を使用して SQLite データベースのデータの読み取り、表示、更新、削除を行う方法について学習します。LazyColumn を使用してデータベースのデータを表示し、データベースを構成するデータが変更されると表示データを自動的に更新します。
前提条件
- Room ライブラリを使用して SQLite データベースの作成と操作ができること。
- エンティティ クラス、DAO クラス、データベース クラスを作成できること。
- データ アクセス オブジェクト(DAO)を使用して Kotlin 関数を SQL クエリにマッピングできること。
LazyColumnでリストアイテムを表示できること。- このユニットで前の Codelab(Room を使用してデータを永続化する)を修了していること。
学習内容
- SQLite データベースからエンティティを読み取り、表示する方法。
- Room ライブラリを使用して SQLite データベースのエンティティの更新と削除を行う方法。
作成するアプリの概要
- 在庫アイテムのリストを表示し、Room を使用してアプリ データベースのアイテムを更新、編集、削除できる Inventory アプリ。
必要なもの
- Android Studio がインストールされているパソコン
2. スターター アプリの概要
この Codelab では、前の Codelab(Room を使用してデータを永続化する)で扱った Inventory アプリの解答コードをスターター コードとして使用します。スターター アプリはすでに、Room 永続ライブラリを使用してデータを保存できる状態になっています。ユーザーは [Add Item] 画面を使用してアプリ データベースにデータを追加できます。
|
|
この Codelab ではアプリを拡張し、Room ライブラリを使用して、データの読み取りと表示、データベースのエンティティの更新と削除を行えるようにします。
この Codelab のスターター コードをダウンロードする
まず、スターター コードをダウンロードします。
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-inventory-app.git $ cd basic-android-kotlin-compose-training-inventory-app $ git checkout room
または、リポジトリを ZIP ファイルとしてダウンロードし、Android Studio で開くこともできます。
この Codelab のスターター コードを確認する場合は、GitHub で表示します。
3. UI 状態を更新する
このタスクではアプリに LazyColumn を追加して、データベースに格納されているデータを表示します。

コンポーズ可能な関数 HomeScreen のチュートリアル
ui/home/HomeScreen.ktファイルを開き、HomeScreen()コンポーザブルを確認します。
@Composable
fun HomeScreen(
navigateToItemEntry: () -> Unit,
navigateToItemUpdate: (Int) -> Unit,
modifier: Modifier = Modifier,
) {
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
Scaffold(
topBar = {
// Top app with app title
},
floatingActionButton = {
FloatingActionButton(
// onClick details
) {
Icon(
// Icon details
)
}
},
) { innerPadding ->
// Display List header and List of Items
HomeBody(
itemList = listOf(), // Empty list is being passed in for itemList
onItemClick = navigateToItemUpdate,
modifier = modifier.padding(innerPadding)
.fillMaxSize()
)
}
このコンポーズ可能な関数は、次のアイテムを表示します。
- アプリタイトルが記載されたトップ アプリバー
- 在庫に新しいアイテムを追加するフローティング アクション ボタン(FAB)

- コンポーズ可能な関数
HomeBody()
コンポーズ可能な関数 HomeBody() は、渡されたリストに基づいて在庫アイテムを表示します。スターター コード実装の一環として、空のリスト(listOf())がコンポーズ可能な関数 HomeBody() に渡されます。在庫リストをこのコンポーザブルに渡すには、リポジトリから在庫データを取得して HomeViewModel に渡す必要があります。
HomeViewModel で UI 状態を出力する
ItemDao にメソッド(getItem() と getAllItems())を追加してアイテムを取得する際、戻り値の型として Flow を指定します。Flow は通常のデータ ストリームを表すということを思い出してください。Flow を返すと、特定のライフサイクルについて DAO からメソッドを明示的に 1 回呼び出すだけで済みます。Room は、基となるデータの更新を非同期的に処理します。
Flow からデータを取得することを「Flow からの収集」といいます。UI レイヤの Flow から収集する際は、考慮すべき点がいくつかあります。
- 構成変更(デバイスの回転など)のようなライフサイクル イベントが発生すると、アクティビティが再作成されます。これにより、再コンポーズと
Flowからの収集が再度行われます。 - ライフサイクル イベント間で既存のデータが失われないように、値を状態としてキャッシュに保存することをおすすめします。
- コンポーザブルのライフサイクルが終了した後など、オブザーバーが残っていない場合は、Flow をキャンセルする必要があります。
ViewModel から Flow を公開するには、StateFlow を使用することをおすすめします。StateFlow を使用すると、UI のライフサイクルに関係なくデータを保存でき、監視できます。Flow を StateFlow に変換するには、stateIn 演算子を使用します。
stateIn 演算子には、次に示す 3 つのパラメータがあります。
scope-viewModelScopeは、StateFlowのライフサイクルを定義します。viewModelScopeがキャンセルされると、StateFlowもキャンセルされます。started- パイプラインは、UI が表示されている場合にのみアクティブにする必要があります。そのためにはSharingStarted.WhileSubscribed()を使用します。最後のサブスクライバーの消失から共有コルーチンの停止までの遅延(ミリ秒単位)を設定するには、TIMEOUT_MILLISをSharingStarted.WhileSubscribed()メソッドに渡します。initialValue- 状態フローの初期値をHomeUiState()に設定します。
Flow を StateFlow に変換したら、collectAsState() メソッドを使用して収集し、そのデータを同じ型の State に変換できます。
このステップでは、StateFlow(UI 状態のオブザーバブル API)として、Room データベース内のアイテムをすべて取得します。Room の在庫データが変更されると、UI が自動的に更新されます。
ui/home/HomeViewModel.ktファイルを開きます。このファイルにはTIMEOUT_MILLIS定数と、コンストラクタ パラメータとしてアイテムのリストが指定されたHomeUiStateデータクラスが含まれています。
// No need to copy over, this code is part of starter code
class HomeViewModel : ViewModel() {
companion object {
private const val TIMEOUT_MILLIS = 5_000L
}
}
data class HomeUiState(val itemList: List<Item> = listOf())
HomeViewModelクラス内で、StateFlow<HomeUiState>型のhomeUiStateというvalを宣言します。 初期化エラーはこの後すぐに解決します。
val homeUiState: StateFlow<HomeUiState>
itemsRepositoryに対してgetAllItemsStream()を呼び出し、宣言したhomeUiStateに割り当てます。
val homeUiState: StateFlow<HomeUiState> =
itemsRepository.getAllItemsStream()
「Unresolved reference: itemsRepository」というエラーが発生します。この未解決の参照エラーを解決するには、ItemsRepository オブジェクトを HomeViewModel に渡す必要があります。
ItemsRepository型のコンストラクタ パラメータをHomeViewModelクラスに追加します。
import com.example.inventory.data.ItemsRepository
class HomeViewModel(itemsRepository: ItemsRepository): ViewModel() {
ui/AppViewModelProvider.ktファイルのHomeViewModelイニシャライザで、次のようにItemsRepositoryオブジェクトを渡します。
initializer {
HomeViewModel(inventoryApplication().container.itemsRepository)
}
HomeViewModel.ktファイルに戻ります。型の不一致エラーが発生します。これを解決するには、次のように変換マップを追加します。
val homeUiState: StateFlow<HomeUiState> =
itemsRepository.getAllItemsStream().map { HomeUiState(it) }
Android Studio に型の不一致エラーが引き続き表示されます。このエラーは、homeUiState が StateFlow 型であり、getAllItemsStream() が Flow を返すことが原因です。
stateIn演算子を使用してFlowをStateFlowに変換します。StateFlowは、UI 状態のオブザーバブル API であり、UI 自体を更新できます。
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
val homeUiState: StateFlow<HomeUiState> =
itemsRepository.getAllItemsStream().map { HomeUiState(it) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS),
initialValue = HomeUiState()
)
- アプリをビルドして、コードにエラーがないことを確認します。表示は変わりません。
4. 在庫データを表示する
このタスクでは、HomeScreen で UI 状態を収集し、更新します。
HomeScreen.ktファイルのコンポーズ可能な関数HomeScreenに、HomeViewModel型の新しい関数パラメータを追加し、初期化します。
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.inventory.ui.AppViewModelProvider
@Composable
fun HomeScreen(
navigateToItemEntry: () -> Unit,
navigateToItemUpdate: (Int) -> Unit,
modifier: Modifier = Modifier,
viewModel: HomeViewModel = viewModel(factory = AppViewModelProvider.Factory)
)
- コンポーズ可能な関数
HomeScreenにhomeUiStateというvalを追加して、HomeViewModelから UI 状態を収集します。collectAsState()を使用します。これにより、StateFlowから値を収集し、Stateを介してその最新の値を表します。
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
val homeUiState by viewModel.homeUiState.collectAsState()
HomeBody()関数呼び出しを更新し、homeUiState.itemListをitemListパラメータに渡します。
HomeBody(
itemList = homeUiState.itemList,
onItemClick = navigateToItemUpdate,
modifier = modifier.padding(innerPadding)
)
- アプリを実行します。アプリ データベースにアイテムを保存した場合、在庫リストが表示されます。リストが空の場合は、在庫アイテムをアプリ データベースに追加します。

5. データベースをテストする
コードをテストする重要性については、これまでの Codelab で説明しました。このタスクでは、DAO クエリをテストする単体テストを追加します。その後、Codelab を進めながらテストを追加します。
データベース実装をテストするには、JUnit テストを作成して Android デバイス上で実行することをおすすめします。このテストではアクティビティの作成が必要ないため、UI テストよりも高速に実行できます。
build.gradle.kts (Module :app)ファイルで、Espresso と JUnit に関する次の依存関係に注意してください。
// Testing
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
- [Project] ビューに切り替え、[src] > [New] > [Directory] を右クリックして、テスト用にテスト ソースセットを作成します。

- [New Directory] ポップアップで [androidTest/kotlin] を選択します。

ItemDaoTest.ktという Kotlin クラスを作成します。ItemDaoTestクラスに@RunWith(AndroidJUnit4::class)アノテーションを付けます。クラスは以下のサンプルコードのようになります。
package com.example.inventory
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ItemDaoTest {
}
- クラス内に、
ItemDao型とInventoryDatabase型のプライベートvar変数を追加します。
import com.example.inventory.data.InventoryDatabase
import com.example.inventory.data.ItemDao
private lateinit var itemDao: ItemDao
private lateinit var inventoryDatabase: InventoryDatabase
- データベースを作成するための関数を追加して
@Beforeアノテーションを付け、どのテストよりも前に実行できるようにします。 - メソッド内で、
itemDaoを初期化します。
import android.content.Context
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import org.junit.Before
@Before
fun createDb() {
val context: Context = ApplicationProvider.getApplicationContext()
// Using an in-memory database because the information stored here disappears when the
// process is killed.
inventoryDatabase = Room.inMemoryDatabaseBuilder(context, InventoryDatabase::class.java)
// Allowing main thread queries, just for testing.
.allowMainThreadQueries()
.build()
itemDao = inventoryDatabase.itemDao()
}
この関数では、インメモリ データベースを使用し、ディスク上には保持しません。そのためには、inMemoryDatabaseBuilder() 関数を使用します。この処理を行うのは、情報を永続的に保持する必要はなく、プロセスが強制終了されたときに削除する必要があるためです。テスト用に、.allowMainThreadQueries() を使用してメインスレッドで DAO クエリを実行しています。
- データベースを閉じるために別の関数を追加します。
@Afterアノテーションを付けて、データベースを閉じ、それぞれのテストの後に実行します。
import org.junit.After
import java.io.IOException
@After
@Throws(IOException::class)
fun closeDb() {
inventoryDatabase.close()
}
- 次のコード例に示すように、使用するデータベース用にクラス
ItemDaoTestでアイテムを宣言します。
import com.example.inventory.data.Item
private var item1 = Item(1, "Apples", 10.0, 20)
private var item2 = Item(2, "Bananas", 15.0, 97)
- データベースにアイテムを 1 つ追加してから 2 つ追加するユーティリティ関数を追加します。これらの関数は後ほどテストで使用します。
suspendとしてマークし、コルーチン内で実行できるようにします。
private suspend fun addOneItemToDb() {
itemDao.insert(item1)
}
private suspend fun addTwoItemsToDb() {
itemDao.insert(item1)
itemDao.insert(item2)
}
- データベースにアイテムを 1 つ挿入する(
insert())テストを作成します。テストにdaoInsert_insertsItemIntoDBという名前を付け、@Testアノテーションを付けます。
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
@Test
@Throws(Exception::class)
fun daoInsert_insertsItemIntoDB() = runBlocking {
addOneItemToDb()
val allItems = itemDao.getAllItems().first()
assertEquals(allItems[0], item1)
}
このテストでは、ユーティリティ関数 addOneItemToDb() を使用してデータベースにアイテムを 1 つ追加します。次に、データベースの最初のアイテムを読み取ります。assertEquals() を使用して想定値と実際値を比較します。runBlocking{} を使用して新しいコルーチンでテストを実行します。このためにユーティリティ関数を suspend としてマークします。
- テストを実行し、合格することを確認します。


- データベースから
getAllItems()の別のテストを作成します。テストにdaoGetAllItems_returnsAllItemsFromDBという名前を付けます。
@Test
@Throws(Exception::class)
fun daoGetAllItems_returnsAllItemsFromDB() = runBlocking {
addTwoItemsToDb()
val allItems = itemDao.getAllItems().first()
assertEquals(allItems[0], item1)
assertEquals(allItems[1], item2)
}
上記のテストでは、コルーチン内でデータベースにアイテムを 2 つ追加します。次に、その 2 つのアイテムを読み取り、想定値と比較します。
6. アイテムの詳細を表示する
このタスクでは、エンティティの詳細を読み取って [Item Details] 画面に表示します。Inventory アプリのデータベースから取得した名前、価格、数量などのアイテム UI 状態を使用し、ItemDetailsScreen コンポーザブルで [Item Details] 画面に表示します。あらかじめ用意されているコンポーズ可能な関数 ItemDetailsScreen には、アイテムの詳細を表示する 3 つの Text コンポーザブルが含まれています。
ui/item/ItemDetailsScreen.kt
この画面はスターター コードの一部であり、アイテムの詳細を表示します。これについては、後の Codelab で説明します。この Codelab では、この画面は扱いません。ItemDetailsViewModel.kt は、この画面に対応する ViewModel です。

- コンポーズ可能な関数
HomeScreenのHomeBody()関数呼び出しに注目してください。navigateToItemUpdateがonItemClickパラメータに渡されています。これは、リスト内のアイテムをクリックすると呼び出されます。
// No need to copy over
HomeBody(
itemList = homeUiState.itemList,
onItemClick = navigateToItemUpdate,
modifier = modifier
.padding(innerPadding)
.fillMaxSize()
)
ui/navigation/InventoryNavGraph.ktを開き、HomeScreenコンポーザブルのnavigateToItemUpdateパラメータに注目してください。このパラメータは、アイテムの詳細画面としてナビゲーションのデスティネーションを指定します。
// No need to copy over
HomeScreen(
navigateToItemEntry = { navController.navigate(ItemEntryDestination.route) },
navigateToItemUpdate = {
navController.navigate("${ItemDetailsDestination.route}/${it}")
}
onItemClick 関数のこの部分は、すでに実装されています。リストアイテムをクリックすると、アプリは [Item Details] 画面に移動します。
- 在庫リストのアイテムをクリックすると、フィールドが空の状態で [Item Details] 画面が表示されます。
![データが空の [Item Details] 画面](https://developer.android.com/static/codelabs/basic-android-kotlin-compose-update-data-room/img/fc38a289ccb8a947.png?authuser=3&hl=ja)
テキスト フィールドにアイテムの詳細を入力するには、ItemDetailsScreen() で UI 状態を収集する必要があります。
UI/Item/ItemDetailsScreen.ktで、ItemDetailsViewModel型のItemDetailsScreenコンポーザブルに新しいパラメータを追加し、ファクトリ メソッドを使用して初期化します。
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.inventory.ui.AppViewModelProvider
@Composable
fun ItemDetailsScreen(
navigateToEditItem: (Int) -> Unit,
navigateBack: () -> Unit,
modifier: Modifier = Modifier,
viewModel: ItemDetailsViewModel = viewModel(factory = AppViewModelProvider.Factory)
)
ItemDetailsScreen()コンポーザブル内にuiStateというvalを作成して、UI 状態を収集します。collectAsState()を使用してuiStateStateFlowを収集し、Stateを介して最新の値を表します。Android Studio に未解決の参照エラーが表示されます。
import androidx.compose.runtime.collectAsState
val uiState = viewModel.uiState.collectAsState()
- エラーを解決するには、
ItemDetailsViewModelクラスにStateFlow<ItemDetailsUiState>型のuiStateというvalを作成します。 - アイテム リポジトリからデータを取得し、拡張関数
toItemDetails()を使用してItemDetailsUiStateにマッピングします。拡張関数Item.toItemDetails()はスターター コードの一部としてすでに作成されています。
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
val uiState: StateFlow<ItemDetailsUiState> =
itemsRepository.getItemStream(itemId)
.filterNotNull()
.map {
ItemDetailsUiState(itemDetails = it.toItemDetails())
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS),
initialValue = ItemDetailsUiState()
)
ItemsRepositoryをItemDetailsViewModelに渡して、Unresolved reference: itemsRepositoryエラーを解決します。
class ItemDetailsViewModel(
savedStateHandle: SavedStateHandle,
private val itemsRepository: ItemsRepository
) : ViewModel() {
- 次のコード スニペットに示すように、
ui/AppViewModelProvider.ktでItemDetailsViewModelのイニシャライザを更新します。
initializer {
ItemDetailsViewModel(
this.createSavedStateHandle(),
inventoryApplication().container.itemsRepository
)
}
ItemDetailsScreen.ktに戻ると、ItemDetailsScreen()コンポーザブルのエラーが解決されています。ItemDetailsScreen()コンポーザブルで、ItemDetailsBody()関数呼び出しを更新し、uiState.valueをitemUiState引数に渡します。
ItemDetailsBody(
itemUiState = uiState.value,
onSellItem = { },
onDelete = { },
modifier = modifier.padding(innerPadding)
)
ItemDetailsBody()とItemInputForm()の実装を確認します。現在選択されているitemをItemDetailsBody()からItemDetails()に渡します。
// No need to copy over
@Composable
private fun ItemDetailsBody(
itemUiState: ItemUiState,
onSellItem: () -> Unit,
onDelete: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
//...
) {
var deleteConfirmationRequired by rememberSaveable { mutableStateOf(false) }
ItemDetails(
item = itemDetailsUiState.itemDetails.toItem(), modifier = Modifier.fillMaxWidth()
)
//...
}
- アプリを実行します。[Inventory] 画面のリスト要素をクリックすると、[Item Details] 画面が表示されます。
- 空白画面にはならず、在庫データベースから取得したエンティティの詳細が表示されます。
![有効なアイテムの詳細が表示された [Item Details] 画面](https://developer.android.com/static/codelabs/basic-android-kotlin-compose-update-data-room/img/b0c839d911d5c379.png?authuser=3&hl=ja)
- [Sell] ボタンをタップします。何も起こりません。
次のセクションでは、[Sell] ボタンの機能を実装します。
7. アイテムの詳細画面を実装する
ui/item/ItemEditScreen.kt
アイテム編集画面は、スターター コードの一部としてすでに用意されています。
このレイアウトには、新しい在庫アイテムの詳細を編集するテキスト フィールド コンポーザブルが含まれています。

このアプリのコードはまだ完全には機能しません。たとえば、[Item Details] 画面で [Sell] ボタンをタップしても [Quantity in Stock] は増えません。[Delete] ボタンをタップすると確認ダイアログが表示されますが、[Yes] ボタンを選択しても、そのアイテムが実際に削除されるわけではありません。

最後に、FAB ボタン
をタップすると空の [Edit Item] 画面が開きます。
![空のフィールドを含む [Edit Item] 画面](https://developer.android.com/static/codelabs/basic-android-kotlin-compose-update-data-room/img/cdccb3a8931b4a3.png?authuser=3&hl=ja)
このセクションでは、[Sell] ボタン、[Delete] ボタン、FAB ボタンの機能を実装します。
8. アイテム販売機能を実装する
このセクションでは、アプリの機能を拡張して販売機能を実装します。この更新では次のタスクを行います。
- DAO 関数のテストを追加してエンティティを更新します。
ItemDetailsViewModelに関数を追加して数量を減らし、アプリ データベースのエンティティを更新します。- 数量が 0 の場合は [Sell] ボタンを無効にします。
ItemDaoTest.ktに、パラメータのないdaoUpdateItems_updatesItemsInDB()という関数を追加します。@Testアノテーションと@Throws(Exception::class)アノテーションを付けます。
@Test
@Throws(Exception::class)
fun daoUpdateItems_updatesItemsInDB()
- 関数を定義して、
runBlockingブロックを作成します。内部でaddTwoItemsToDb()を呼び出します。
fun daoUpdateItems_updatesItemsInDB() = runBlocking {
addTwoItemsToDb()
}
- 2 つのエンティティを異なる値で更新し、
itemDao.updateを呼び出します。
itemDao.update(Item(1, "Apples", 15.0, 25))
itemDao.update(Item(2, "Bananas", 5.0, 50))
itemDao.getAllItems()を使用してエンティティを取得します。更新したエンティティと比較し、アサートします。
val allItems = itemDao.getAllItems().first()
assertEquals(allItems[0], Item(1, "Apples", 15.0, 25))
assertEquals(allItems[1], Item(2, "Bananas", 5.0, 50))
- 完成した関数が次のようになっていることを確認します。
@Test
@Throws(Exception::class)
fun daoUpdateItems_updatesItemsInDB() = runBlocking {
addTwoItemsToDb()
itemDao.update(Item(1, "Apples", 15.0, 25))
itemDao.update(Item(2, "Bananas", 5.0, 50))
val allItems = itemDao.getAllItems().first()
assertEquals(allItems[0], Item(1, "Apples", 15.0, 25))
assertEquals(allItems[1], Item(2, "Bananas", 5.0, 50))
}
- テストを実行し、合格することを確認します。
ViewModel に関数を追加する
ItemDetailsViewModel.ktのItemDetailsViewModelクラス内に、パラメータのないreduceQuantityByOne()という関数を追加します。
fun reduceQuantityByOne() {
}
- この関数内で、
viewModelScope.launch{}を使用してコルーチンを開始します。
import kotlinx.coroutines.launch
import androidx.lifecycle.viewModelScope
viewModelScope.launch {
}
launchブロック内で、currentItemというvalを作成し、uiState.value.toItem()に設定します。
val currentItem = uiState.value.toItem()
uiState.value は ItemUiState 型です。拡張関数 toItem() を使用して、Item エンティティ型に変換します。
qualityが0より大きいかどうかを確認するifステートメントを追加します。itemsRepositoryに対してupdateItem()を呼び出し、更新したcurrentItemを渡します。copy()を使用してquantity値を更新し、関数を次のようにします。
fun reduceQuantityByOne() {
viewModelScope.launch {
val currentItem = uiState.value.itemDetails.toItem()
if (currentItem.quantity > 0) {
itemsRepository.updateItem(currentItem.copy(quantity = currentItem.quantity - 1))
}
}
}
ItemDetailsScreen.ktに戻ります。ItemDetailsScreenコンポーザブルで、ItemDetailsBody()関数呼び出しに移動します。onSellItemラムダで、viewModel.reduceQuantityByOne()を呼び出します。
ItemDetailsBody(
itemUiState = uiState.value,
onSellItem = { viewModel.reduceQuantityByOne() },
onDelete = { },
modifier = modifier.padding(innerPadding)
)
- アプリを実行します。
- [Inventory] 画面でリスト要素をクリックします。[Item Details] 画面が表示されたら、[Sell] をタップすると数量の値が 1 減ります。
![[Item Details] 画面で [Sell] ボタンをタップすると数量が 1 減る](https://developer.android.com/static/codelabs/basic-android-kotlin-compose-update-data-room/img/3aac7e2c9e7a04b6.png?authuser=3&hl=ja)
- [Item Details] 画面で、数量が 0 になるまで [Sell] ボタンをタップし続けます。
数量が 0 になったら、再度 [Sell] をタップします。数量を更新する前に関数 reduceQuantityByOne() によって数量が 0 より大きいかどうかがチェックされるため、見た目は変わりません。
![在庫量が 0 の [Item Details] 画面](https://developer.android.com/static/codelabs/basic-android-kotlin-compose-update-data-room/img/dbd889a1ac1f3be4.png?authuser=3&hl=ja)
適切なフィードバックをユーザーに提供するために、販売するアイテムがないときは [Sell] ボタンを無効にすることをおすすめします。
ItemDetailsViewModelクラスで、map変換のit.quantityに基づいてoutOfStock値を設定します。
val uiState: StateFlow<ItemDetailsUiState> =
itemsRepository.getItemStream(itemId)
.filterNotNull()
.map {
ItemDetailsUiState(outOfStock = it.quantity <= 0, itemDetails = it.toItemDetails())
}.stateIn(
//...
)
- アプリを実行します。在庫量が 0 になると [Sell] ボタンが無効になります。
![[Sell] ボタンが無効になっている [Item Details] 画面](https://developer.android.com/static/codelabs/basic-android-kotlin-compose-update-data-room/img/48f2748adfe30d47.png?authuser=3&hl=ja)
これで、アイテム販売機能がアプリに実装されました。
アイテム エンティティを削除する
前のタスクと同様に、削除機能を実装して、アプリの機能をさらに拡張する必要があります。この機能は、販売機能よりはるかに簡単に実装できます。このプロセスでは次のタスクを行います。
- 削除 DAO クエリのテストを追加します。
- データベースからエンティティを削除する関数を
ItemDetailsViewModelクラスに追加します。 ItemDetailsBodyコンポーザブルを更新します。
DAO テストを追加する
ItemDaoTest.ktに、daoDeleteItems_deletesAllItemsFromDB()というテストを追加します。
@Test
@Throws(Exception::class)
fun daoDeleteItems_deletesAllItemsFromDB()
runBlocking {}を使用してコルーチンを開始します。
fun daoDeleteItems_deletesAllItemsFromDB() = runBlocking {
}
- データベースにアイテムを 2 つ追加し、この 2 つのアイテムに対して
itemDao.delete()を呼び出してデータベースから削除します。
addTwoItemsToDb()
itemDao.delete(item1)
itemDao.delete(item2)
- データベースからエンティティを取得し、リストが空であることを確認します。完成したテストは次のようになります。
import org.junit.Assert.assertTrue
@Test
@Throws(Exception::class)
fun daoDeleteItems_deletesAllItemsFromDB() = runBlocking {
addTwoItemsToDb()
itemDao.delete(item1)
itemDao.delete(item2)
val allItems = itemDao.getAllItems().first()
assertTrue(allItems.isEmpty())
}
ItemDetailsViewModel に削除関数を追加する
ItemDetailsViewModelに、deleteItem()という新しい関数を追加します。これはパラメータを受け取らず、何も返しません。deleteItem()関数内にitemsRepository.deleteItem()関数呼び出しを追加して、uiState.value.toItem()を渡します。
suspend fun deleteItem() {
itemsRepository.deleteItem(uiState.value.itemDetails.toItem())
}
この関数では、toItem() 拡張関数を使用して、uiState を itemDetails 型から Item エンティティ型に変換します。
ui/item/ItemDetailsScreenコンポーザブルに、coroutineScopeというvalを追加し、rememberCoroutineScope()に設定します。このアプローチでは、呼び出されたコンポジション(ItemDetailsScreenコンポーザブル)にバインドされているコルーチン スコープが返されます。
import androidx.compose.runtime.rememberCoroutineScope
val coroutineScope = rememberCoroutineScope()
ItemDetailsBody()関数までスクロールします。onDeleteラムダ内でcoroutineScopeを使用してコルーチンを開始します。launchブロック内で、viewModelに対してdeleteItem()メソッドを呼び出します。
import kotlinx.coroutines.launch
ItemDetailsBody(
itemUiState = uiState.value,
onSellItem = { viewModel.reduceQuantityByOne() },
onDelete = {
coroutineScope.launch {
viewModel.deleteItem()
}
modifier = modifier.padding(innerPadding)
)
- アイテムを削除したら、在庫画面に戻ります。
deleteItem()関数呼び出しの後でnavigateBack()を呼び出します。
onDelete = {
coroutineScope.launch {
viewModel.deleteItem()
navigateBack()
}
- 引き続き
ItemDetailsScreen.ktファイル内で、ItemDetailsBody()関数までスクロールします。
この関数はスターター コードの一部です。このコンポーザブルは、アイテムを削除する前にユーザーの確認を得るためのアラート ダイアログを表示します。[Yes] をタップすると deleteItem() 関数が呼び出されます。
// No need to copy over
@Composable
private fun ItemDetailsBody(
itemUiState: ItemUiState,
onSellItem: () -> Unit,
onDelete: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
/*...*/
) {
//...
if (deleteConfirmationRequired) {
DeleteConfirmationDialog(
onDeleteConfirm = {
deleteConfirmationRequired = false
onDelete()
},
//...
)
}
}
}
[No] をタップするとアラート ダイアログが閉じます。showConfirmationDialog() 関数によって次のアラートが表示されます。

- アプリを実行します。
- [Inventory] 画面でリスト要素を選択します。
- [Item Details] 画面で、[Delete] をタップします。
- アラート ダイアログで [Yes] をタップすると [Inventory] 画面に戻ります。
- 削除したエンティティがアプリ データベースに存在しなくなっていることを確認します。
これで削除機能が実装されました。
|
|
アイテム エンティティを編集する
前のセクションと同様に、このセクションではアイテム エンティティを編集するという別の機能をアプリに追加します。
アプリ データベースのエンティティを編集する手順を簡単に示すと、次のようになります。
- テスト用アイテム取得 DAO クエリにテストを追加します。
- テキスト フィールドと [Edit Item] 画面にエンティティの詳細を入力します。
- Room を使用してデータベースのエンティティを更新します。
DAO テストを追加する
ItemDaoTest.ktに、daoGetItem_returnsItemFromDB()というテストを追加します。
@Test
@Throws(Exception::class)
fun daoGetItem_returnsItemFromDB()
- 関数を定義します。コルーチン内で、データベースにアイテムを 1 つ追加します。
@Test
@Throws(Exception::class)
fun daoGetItem_returnsItemFromDB() = runBlocking {
addOneItemToDb()
}
itemDao.getItem()関数を使用してデータベースからエンティティを取得し、itemというvalに設定します。
val item = itemDao.getItem(1)
assertEquals()を使用して実際値と取得値を比較し、アサートします。完成したテストは次のようになります。
@Test
@Throws(Exception::class)
fun daoGetItem_returnsItemFromDB() = runBlocking {
addOneItemToDb()
val item = itemDao.getItem(1)
assertEquals(item.first(), item1)
}
- テストを実行し、合格することを確認します。
テキスト フィールドに入力する
アプリを実行して [Item Details] 画面に移動し、次に FAB をクリックすると、画面のタイトルが [Edit Item] になったことを確認できます。ただし、テキスト フィールドはすべて空です。このステップでは、[Edit Item] 画面のテキスト フィールドにエンティティの詳細を入力します。
|
|
ItemDetailsScreen.ktで、ItemDetailsScreenコンポーザブルまでスクロールします。FloatingActionButton()で、onClick引数を変更してuiState.value.itemDetails.id(選択したエンティティのid)を含めます。このidを使用してエンティティの詳細を取得します。
FloatingActionButton(
onClick = { navigateToEditItem(uiState.value.itemDetails.id) },
modifier = /*...*/
)
ItemEditViewModelクラスに、initブロックを追加します。
init {
}
initブロック内で、viewModelScope.launchを使用してコルーチンを開始します。
import kotlinx.coroutines.launch
viewModelScope.launch { }
launchブロック内で、itemsRepository.getItemStream(itemId)を使用してエンティティの詳細を取得します。
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
init {
viewModelScope.launch {
itemUiState = itemsRepository.getItemStream(itemId)
.filterNotNull()
.first()
.toItemUiState(true)
}
}
この launch ブロックに、null でない値のみを含むフローを返すフィルタを追加します。toItemUiState() を使用して、item エンティティを ItemUiState に変換します。actionEnabled 値を true として渡し、[Save] ボタンを有効にします。
Unresolved reference: itemsRepository エラーを解決するには、ビューモデルに依存関係として ItemsRepository を渡す必要があります。
ItemEditViewModelクラスにコンストラクタ パラメータを追加します。
class ItemEditViewModel(
savedStateHandle: SavedStateHandle,
private val itemsRepository: ItemsRepository
)
AppViewModelProvider.ktファイルのItemEditViewModelイニシャライザで、ItemsRepositoryオブジェクトを引数として追加します。
initializer {
ItemEditViewModel(
this.createSavedStateHandle(),
inventoryApplication().container.itemsRepository
)
}
- アプリを実行します。
- [Item Details] に移動し、FAB
をタップします。 - フィールドにアイテムの詳細が入力されます。
- 在庫量または他のフィールドを編集し、[Save] をタップします。
何も起こりません。アプリ データベースのエンティティを更新していないためです。これは次のセクションで修正します。
|
|
Room を使用してエンティティを更新する
この最後のタスクでは、コードの最後の要素を追加して更新機能を実装します。ViewModel で必要な関数を定義し、ItemEditScreen で使用します。
それではまたコーディングしてみましょう。
ItemEditViewModelクラスに、ItemUiStateオブジェクトを受け取って何も返さないupdateUiState()という関数を追加します。この関数は、ユーザーが入力した新しい値でitemUiStateを更新します。
fun updateUiState(itemDetails: ItemDetails) {
itemUiState =
ItemUiState(itemDetails = itemDetails, isEntryValid = validateInput(itemDetails))
}
この関数では、渡された itemDetails を itemUiState に割り当て、isEntryValid 値を更新します。itemDetails が true の場合、[Save] ボタンが有効になります。ユーザーが入力した内容が有効である場合にのみ、この値を true に設定します。
ItemEditScreen.ktファイルに移動します。ItemEditScreenコンポーザブルで、ItemEntryBody()関数呼び出しまで下にスクロールします。onItemValueChange引数の値を、新しい関数updateUiStateに設定します。
ItemEntryBody(
itemUiState = viewModel.itemUiState,
onItemValueChange = viewModel::updateUiState,
onSaveClick = { },
modifier = modifier.padding(innerPadding)
)
- アプリを実行します。
- [Edit Item] 画面に移動します。
- 無効になるようにエンティティ値のいずれかを空にします。[Save] ボタンが自動的に無効になります。
|
|
|
ItemEditViewModelクラスに戻り、何も受け取らないupdateItem()というsuspend関数を追加します。この関数を使用して、更新したエンティティを Room データベースに保存します。
suspend fun updateItem() {
}
getUpdatedItemEntry()関数内で、if条件を追加し、validateInput()関数を使用してユーザー入力を検証します。itemsRepositoryに対してupdateItem()関数を呼び出し、itemUiState.itemDetails.toItem()を渡します。Room データベースに追加するには、エンティティをItem型にする必要があります。完成した関数は次のようになります。
suspend fun updateItem() {
if (validateInput(itemUiState.itemDetails)) {
itemsRepository.updateItem(itemUiState.itemDetails.toItem())
}
}
ItemEditScreenコンポーザブルに戻ります。updateItem()関数を呼び出すためにコルーチン スコープが必要です。coroutineScopeという val を作成し、rememberCoroutineScope()に設定します。
import androidx.compose.runtime.rememberCoroutineScope
val coroutineScope = rememberCoroutineScope()
ItemEntryBody()関数呼び出しで、onSaveClick関数の引数を更新し、coroutineScope内のコルーチンを開始します。launchブロック内で、viewModelに対してupdateItem()を呼び出し、戻ります。
import kotlinx.coroutines.launch
onSaveClick = {
coroutineScope.launch {
viewModel.updateItem()
navigateBack()
}
},
完成した ItemEntryBody() 関数呼び出しは次のようになります。
ItemEntryBody(
itemUiState = viewModel.itemUiState,
onItemValueChange = viewModel::updateUiState,
onSaveClick = {
coroutineScope.launch {
viewModel.updateItem()
navigateBack()
}
},
modifier = modifier.padding(innerPadding)
)
- アプリを実行し、在庫アイテムを編集してみましょう。Inventory アプリのデータベース内のアイテムを編集できるようになりました。
|
|
Room を使用してデータベースを管理する初めてのアプリを作成しました。
9. 解答コード
この Codelab の解答コードは、以下に示す GitHub リポジトリとブランチにあります。
10. 関連リンク
Android デベロッパー ドキュメント
- Database Inspector を使用してデータベースをデバッグする
- Room を使用してローカル データベースにデータを保存する
- データベースをテストしてデバッグする | Android デベロッパー
Kotlin リファレンス
![アイテムの詳細が入力された [Add Item] 画面](https://developer.android.com/static/codelabs/basic-android-kotlin-compose-update-data-room/img/bae9fd572d154881.png?authuser=3&hl=ja)

![[Alerts] ダイアログ ウィンドウが表示された [Item Details] 画面。](https://developer.android.com/static/codelabs/basic-android-kotlin-compose-update-data-room/img/5a03d33f03b4d17c.png?authuser=3&hl=ja)

![[Sell] ボタンが有効になっている [Item Details] 画面](https://developer.android.com/static/codelabs/basic-android-kotlin-compose-update-data-room/img/d368151eb7b198cd.png?authuser=3&hl=ja)
![テキスト フィールドがすべて入力され、[Save] ボタンが有効になっている [Edit Item] 画面](https://developer.android.com/static/codelabs/basic-android-kotlin-compose-update-data-room/img/427ff7e2bf45f6ca.png?authuser=3&hl=ja)
![[Save] ボタンが無効な [Edit Item] 画面](https://developer.android.com/static/codelabs/basic-android-kotlin-compose-update-data-room/img/9aa8fa86a928e1a6.png?authuser=3&hl=ja)
![[Edit Item] 画面のアイテムの詳細を編集しました](https://developer.android.com/static/codelabs/basic-android-kotlin-compose-update-data-room/img/6ed9dac5d3cafeda.png?authuser=3&hl=ja)
![更新されたアイテムの詳細が表示された [アイテムの詳細] 画面](https://developer.android.com/static/codelabs/basic-android-kotlin-compose-update-data-room/img/476f37623617d192.png?authuser=3&hl=ja)