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.google.cn/static/codelabs/basic-android-kotlin-compose-update-data-room/img/fc38a289ccb8a947.png?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()を使用して- uiState- StateFlowを収集し、- 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.google.cn/static/codelabs/basic-android-kotlin-compose-update-data-room/img/b0c839d911d5c379.png?hl=ja)
- [Sell] ボタンをタップします。何も起こりません。
次のセクションでは、[Sell] ボタンの機能を実装します。
7. アイテムの詳細画面を実装する
ui/item/ItemEditScreen.kt
アイテム編集画面は、スターター コードの一部としてすでに用意されています。
このレイアウトには、新しい在庫アイテムの詳細を編集するテキスト フィールド コンポーザブルが含まれています。

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

最後に、FAB ボタン  をタップすると空の [Edit Item] 画面が開きます。
 をタップすると空の [Edit Item] 画面が開きます。
![空のフィールドを含む [Edit Item] 画面](https://developer.android.google.cn/static/codelabs/basic-android-kotlin-compose-update-data-room/img/cdccb3a8931b4a3.png?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.google.cn/static/codelabs/basic-android-kotlin-compose-update-data-room/img/3aac7e2c9e7a04b6.png?hl=ja)
- [Item Details] 画面で、数量が 0 になるまで [Sell] ボタンをタップし続けます。
数量が 0 になったら、再度 [Sell] をタップします。数量を更新する前に関数 reduceQuantityByOne() によって数量が 0 より大きいかどうかがチェックされるため、見た目は変わりません。
![在庫量が 0 の [Item Details] 画面](https://developer.android.google.cn/static/codelabs/basic-android-kotlin-compose-update-data-room/img/dbd889a1ac1f3be4.png?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.google.cn/static/codelabs/basic-android-kotlin-compose-update-data-room/img/48f2748adfe30d47.png?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.google.cn/static/codelabs/basic-android-kotlin-compose-update-data-room/img/bae9fd572d154881.png?hl=ja)

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

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