1. 始める前に
製品版品質のアプリには通常、ユーザーがアプリを閉じた後も保存しておく必要のあるデータがあります。たとえば、曲の再生リスト、To-Do リストの項目、収支の記録、星座表、個人データの履歴などが挙げられます。ほとんどの場合、この永続データの保存にはデータベースが使用されます。
Room は、Android Jetpack の一部である永続ライブラリで、SQLite データベースの上に位置する抽象化レイヤです。SQLite は専門言語(SQL)を使用してデータベース操作を行います。SQLite を直接使用する代わりに Room を使用すると、データベースのセットアップ、設定、操作が簡単になります。Room には、SQLite ステートメントのコンパイル時チェック機能もあります。
下図に、このコースで推奨されているアーキテクチャ全体における Room の位置付けを示します。

前提条件
- Android アプリの基本的なユーザー インターフェース(UI)の作成方法を理解している。
- アクティビティ、フラグメント、ビューの使用方法を理解している。
- フラグメント間を移動する方法、フラグメント間でデータを渡すために Safe Args を使用する方法を理解している。
- Android アーキテクチャ コンポーネントの ViewModel、LiveData、Flowに精通しており、ViewModelProvider.Factoryを使用して ViewModel をインスタンス化する方法を理解している。
- 同時実行の基本に精通している。
- 長時間実行タスクにコルーチンを使用する方法を理解している。
- SQL データベースと SQLite 言語に関する基礎知識がある。
学習内容
- Room ライブラリを使用して SQLite データベースを作成し、操作する方法。
- エンティティ クラス、DAO クラス、データベース クラスを作成する方法。
- データ アクセス オブジェクト(DAO)を使用して Kotlin 関数を SQL クエリにマッピングする方法。
作成するアプリの概要
- インベントリ アイテムを SQLite データベースに保存する Inventory アプリを作成します。
必要なもの
- Inventory アプリのスターター コード。
- Android Studio がインストールされているパソコン
2. アプリの概要
この Codelab では、Inventory アプリというスターター アプリを扱い、Room ライブラリを使用してアプリにデータベース レイヤを追加します。アプリの最終バージョンでは、RecyclerView を使用してインベントリ データベースからリストアイテムを表示します。ユーザーは、新しいアイテムの追加、既存のアイテムの更新、インベントリ データベースからのアイテムの削除ができます(アプリの機能は次回の Codelab で完成します)。
アプリの最終バージョンのスクリーンショットを次に示します。

3.スターター アプリの概要
この Codelab のスターター コードをダウンロードする
この Codelab では、ここで学んだ機能を使って拡張するためのスターター コードが提供されます。スターター コードには、以前の Codelab で学んだコードだけでなく、今後の Codelab で学ぶ予定の、見慣れないコードが含まれていることもあります。
GitHub のスターター コードを使用する場合、フォルダ名は android-basics-kotlin-inventory-app-starter です。Android Studio でプロジェクトを開くときは、このフォルダを選択してください。
この 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] ツール ウィンドウでプロジェクト ファイルを見て、アプリがどのように設定されているかを確認します。
スターター コードの概要
- Android Studio でスターター コードのプロジェクトを開きます。
- Android デバイスまたはエミュレータでアプリを実行します。エミュレータまたは接続済みのデバイスが API レベル 26 以降を搭載していることを確認します。Database Inspector は、API レベル 26 を搭載したエミュレータやデバイスで最適に機能します。
- アプリにインベントリ データは表示されません。データベースに新しいアイテムを追加する FAB に注目します。
- FAB をクリックします。自動で新しい画面に移動し、新しいアイテムの詳細情報を入力できるようになります。

スターター コードの問題点
- [Add Item] 画面でアイテムの詳細を入力します。[SAVE] をタップします。add item フラグメントが閉じられていません。システムの戻るキーを使って戻ります。新しいアイテムは保存されず、インベントリ画面に表示されません。アプリが不完全であり、[SAVE] ボタンの機能が実装されていません。

この Codelab では、インベントリの詳細を SQLite データベースに保存する、アプリのデータベース部分を追加します。Room 永続ライブラリを使用して SQLite データベースを操作します。
コードのチュートリアル
ダウンロードしたスターター コードには、画面のレイアウトがあらかじめ用意されています。ここでは、データベース ロジックの実装に焦点を当てます。作業の土台とするファイルの一部について簡単に説明します。
main_activity.xml
アプリ内の他のすべてのフラグメントをホストするメイン アクティビティ。onCreate() メソッドは NavHostFragment から NavController を取得し、NavController で使用するアクションバーを設定します。
item_list_fragment.xml
アプリで最初に表示される画面。RecyclerView と FAB が主な要素です。RecyclerView は後ほど実装します。
fragment_add_item.xml
このレイアウトには、追加する新しいインベントリ アイテムの詳細を入力するためのテキスト フィールドが含まれています。
ItemListFragment.kt
このフラグメントは大部分がボイラープレート コードです。onViewCreated() メソッドで、クリック リスナーが FAB に設定され、add item フラグメントに移動します。
AddItemFragment.kt
このフラグメントは、データベースに新しいアイテムを追加するために使用します。onCreateView() 関数はバインディング変数を初期化し、onDestroyView() 関数はキーボードを非表示にしてからフラグメントを破棄します。
4. Room の主なコンポーネント
Kotlin は、データクラスを導入することで、データを簡単に処理できるようにします。このデータは、関数呼び出しを使用してアクセスされ、場合によっては変更されます。しかしデータベースの世界では、データに対するアクセスと変更には「テーブル」と「クエリ」が必要です。Room の以下のコンポーネントを使用すると、こうしたワークフローがシームレスになります。
Room は、次の 3 つの主要コンポーネントで構成されます。
- データ エンティティは、アプリのデータベースのテーブルを表します。テーブルの行に格納されているデータの更新や、挿入するための新しい行の作成に使用します。
- データ アクセス オブジェクト(DAO)は、データベース内のデータを取得、更新、挿入、削除するためにアプリで使用するメソッドを提供します。
- データベース クラスは、データベースを保持するものであり、アプリのデータベースに対する基礎的な接続のメイン アクセス ポイントです。データベース クラスは、そのデータベースに関連付けられている DAO のインスタンスをアプリに提供します。
これらのコンポーネントの実装と詳細については、この Codelab で後ほど説明します。下図に、Room のコンポーネントが連携してデータベースを操作する仕組みを示します。

Room ライブラリを追加する
このタスクでは、必要な Room コンポーネント ライブラリを Gradle ファイルに追加します。
- モジュール レベルの Gradle ファイル build.gradle (Module: InventoryApp.app)を開きます。dependenciesブロックで、Room ライブラリについて次の依存関係を追加します。
    // Room
    implementation "androidx.room:room-runtime:$room_version"
    kapt "androidx.room:room-compiler:$room_version"
    implementation "androidx.room:room-ktx:$room_version"
5. アイテム エンティティを作成する
Entity クラスはテーブルを定義します。このクラスの各インスタンスは、データベース テーブルの行を表します。エンティティ クラスには、データベース内の情報の表示方法や操作方法を Room に伝えるためのマッピングがあります。今回のアプリでは、エンティティはアイテム名、アイテム価格、利用可能な在庫など、インベントリ アイテムに関する情報を保持します。

@Entity アノテーションは、クラスをデータベースの Entity クラスとしてマークします。アイテムを保持するためのデータベース テーブルが Entity クラスごとに作成されます。Entity の各フィールドは、特に明記されていない限り、データベースの列として表されます(詳細については Entity のドキュメントをご覧ください)。データベースに格納されるすべてのエンティティ インスタンスに主キーが必要です。主キーは、データベース テーブルのすべてのレコードやエントリを一意に識別するために使用します。一度割り当てた主キーは変更できず、データベース内に存在する限り、エンティティ オブジェクトを表します。
このタスクでは、Entity クラスを作成します。アイテムごとに次のインベントリ情報を格納するフィールドを定義します。
- 主キーを格納する Int。
- アイテム名を格納する String。
- アイテム価格を格納する double。
- 在庫数を格納する Int。
- Android Studio でスターター コードを開きます。
- com.example.inventory基本パッケージの下に- dataというパッケージを作成します。

- dataパッケージ内に- Itemという Kotlin クラスを作成します。このクラスは、アプリ内のデータベース エンティティを表します。次のステップでは、インベントリ情報を格納するために対応するフィールドを追加します。
- 次のコードを使用して Itemクラスの定義を更新します。プライマリ コンストラクタのパラメータとして、Int型のid、String,型のitemName、Double型のitemPrice、Int型のquantityInStockを宣言します。idにデフォルト値0を割り当てます。これが主キー、つまりItemテーブルのすべてのレコードやエントリを一意に識別する ID となります。
class Item(
   val id: Int = 0,
   val itemName: String,
   val itemPrice: Double,
   val quantityInStock: Int
)
データクラス
データクラスは、主に Kotlin でデータを保持するために使用します。data キーワードでマークされています。Kotlin のデータクラス オブジェクトには、コンパイラが比較、出力、コピーのためのユーティリティ(toString()、copy()、equals() など)を自動的に生成するというメリットがあります。
例:
// Example data class with 2 properties.
data class User(val first_name: String, val last_name: String){
}
生成されるコードに一貫性を持たせ、有意義な動作をさせるために、データクラスは次の要件を満たす必要があります。
- プライマリ コンストラクタには少なくとも 1 つのパラメータが必要です。
- プライマリ コンストラクタのパラメータはすべて、valまたはvarとしてマークする必要があります。
- データクラスを abstract、open、sealed、innerにすることはできません。
データクラスの詳細については、こちらのドキュメントをご覧ください。
- クラス定義の前に dataキーワードを付けて、Itemクラスをデータクラスに変換します。
data class Item(
   val id: Int = 0,
   val itemName: String,
   val itemPrice: Double,
   val quantityInStock: Int
)
- Itemクラス宣言の上で、データクラスに- @Entityアノテーションを付けます。- tableName引数を使用して、- itemを SQLite テーブル名として指定します。
@Entity(tableName = "item")
data class Item(
   ...
)
- idを主キーとして識別するには、- idプロパティに- @PrimaryKeyアノテーションを付けます。- Roomが各エンティティの ID を生成するように、パラメータ- autoGenerateを- trueに設定します。これにより、各アイテムの ID が一意になります。
@Entity(tableName = "item")
data class Item(
   @PrimaryKey(autoGenerate = true)
   val id: Int = 0,
   ...
)
- 残りのプロパティに @ColumnInfoアノテーションを付けます。ColumnInfoアノテーションは、特定のフィールドに関連付けられた列をカスタマイズするために使用します。たとえばname引数を使用する場合、フィールドに変数名ではなく列名を指定できます。次のように、パラメータを使用してプロパティ名をカスタマイズします。この方法は、tableNameを使用してデータベースに別の名前を指定する方法に似ています。
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class Item(
   @PrimaryKey(autoGenerate = true)
   val id: Int = 0,
   @ColumnInfo(name = "name")
   val itemName: String,
   @ColumnInfo(name = "price")
   val itemPrice: Double,
   @ColumnInfo(name = "quantity")
   val quantityInStock: Int
)
6. アイテム DAO を作成する
データ アクセス オブジェクト(DAO)
データ アクセス オブジェクト(DAO)は、抽象インターフェースを提供することで永続化レイヤをアプリの残りの部分と分離するために使用するパターンです。この分離は、これまでの Codelab で見てきた単一責任の原則に則したものです。
DAO の機能は、基となる永続化レイヤでのデータベース操作に関連するすべての複雑さを、アプリの残りの部分から隠すことです。これにより、データを使用するコードから独立してデータアクセス レイヤを変更できます。

このタスクでは、Room のデータ アクセス オブジェクト(DAO)を定義します。データ アクセス オブジェクトは、データベースにアクセスするインターフェースを定義する Room の主要コンポーネントです。
作成する DAO は、データベースに対してクエリ(取得)、挿入、削除、更新を行うための便利なメソッドを提供するカスタム インターフェースになります。Room はコンパイル時にこのクラスの実装を生成します。
一般的なデータベース操作の場合、Room ライブラリには @Insert、@Delete、@Update などの便利なアノテーションが用意されています。それ以外の場合は、@Query アノテーションがあります。SQLite でサポートされている、あらゆるクエリを記述できます。
さらに、Android Studio でクエリを記述すると、コンパイラが SQL クエリの構文エラーをチェックします。
このインベントリ アプリの場合、次のことを行える必要があります。
- 新しいアイテムの挿入または追加。
- 既存のアイテムを更新して、名前、価格、数量を更新する。
- 主キーである idに基づいて、特定のアイテムを取得する。
- すべてのアイテムを取得して、表示できるようにする。
- データベースのエントリを削除する。

それでは、アプリにアイテム DAO を実装します。
- dataパッケージで、Kotlin クラス- ItemDao.ktを作成します。
- クラス定義を interfaceに変更し、@Daoアノテーションを付けます。
@Dao
interface ItemDao {
}
- インターフェースの本文内に @Insertアノテーションを追加します。@Insertの下に、Entityクラスのitemのインスタンスを引数として取るinsert()関数を追加します。データベース操作は実行に時間がかかる可能性があるため、別のスレッドで実行する必要があります。関数を suspend 関数にして、コルーチンから呼び出せるようにします。
@Insert
suspend fun insert(item: Item)
- 引数 OnConflictを追加し、値OnConflictStrategy.IGNOREを割り当てます。引数OnConflictは、競合が発生した場合の処理を Room に伝えます。OnConflictStrategy.IGNORE戦略は、主キーがデータベースにすでに存在する場合、新しいアイテムを無視します。利用可能な競合戦略について詳しくは、こちらのドキュメントをご覧ください。
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(item: Item)
これで、item をデータベースに挿入するために必要なすべてのコードが Room によって生成されるようになりました。Kotlin コードから insert() を呼び出すと、Room が SQL クエリを実行し、エンティティをデータベースに挿入します(注: 関数には任意の名前を付けることができます。insert() である必要はありません)。
- 1 つの item用にupdate()関数と@Updateアノテーションを追加します。渡されるエンティティと同じキーを持つエンティティが更新されます。エンティティの他のプロパティの一部または全部を更新できます。insert()メソッドと同様に、次のupdate()メソッドをsuspendにします。
@Update
suspend fun update(item: Item)
- アイテムを削除するために、@Deleteアノテーションとdelete()関数を追加します。サスペンド メソッドにします。@Deleteアノテーションは、1 つまたは複数のアイテムを削除します(注: 削除するエンティティを渡す必要があります。エンティティがない場合は、delete()関数を呼び出す前に取得する必要があります)。
@Delete
suspend fun delete(item: Item)
残りの機能には便利なアノテーションがないため、@Query アノテーションを使用して SQLite クエリを指定する必要があります。
- 指定した idに基づいてアイテム テーブルから特定のアイテムを取得する SQLite クエリを記述します。その後、Room アノテーションを追加して、後のステップで次のクエリの修正版を使用します。次のステップでは、Room を使用してこれを DAO メソッドに変更します。
- itemからすべての列を選択します。
- WHERE句で- idを特定の値と一致させます。
例:
SELECT * from item WHERE id = 1
- 上の SQL クエリを、Room アノテーションと引数で使用するように変更します。@Queryアノテーションを追加し、クエリを文字列パラメータとして@Queryアノテーションに指定します。アイテム テーブルからアイテムを取得する SQLite クエリを、Stringパラメータとして@Queryに追加します。
- itemからすべての列を選択します。
- WHERE句で- idを :- id引数と一致させます。- :idに注目してください。クエリ内でコロン表記を使用して、関数内の引数を参照しています。
@Query("SELECT * from item WHERE id = :id")
- @Queryアノテーションの下に、- Int引数を受け取って- Flow<Item>を返す- getItem()関数を追加します。
@Query("SELECT * from item WHERE id = :id")
fun getItem(id: Int): Flow<Item>
戻り値の型として Flow または LiveData を使用すると、データベース内のデータが変更されるたびに通知を受けることができます。永続化レイヤでは Flow を使用することをおすすめします。Room がこの Flow を最新の状態に維持します。つまり、データを明示的に取得する必要があるのは一度だけです。これは、次の Codelab で実装するインベントリ リストを更新する際に役立ちます。戻り値の型が Flow であるため、Room はバックグラウンド スレッドでクエリを実行します。明示的に suspend 関数にしてコルーチン スコープ内で呼び出す必要はありません。
場合によっては、kotlinx.coroutines.flow.Flow から Flow をインポートする必要があります。
- @Queryと- getItems()関数を追加します。
- SQLite クエリが itemテーブルのすべての列を昇順で返すようにします。
- getItems()が- Itemエンティティのリストを- Flowとして返すようにします。- Roomがこの- Flowを最新の状態に維持します。つまり、データを明示的に取得する必要があるのは一度だけです。
@Query("SELECT * from item ORDER BY name ASC")
fun getItems(): Flow<List<Item>>
- 目に見える変化はありませんが、アプリを実行してエラーがないことを確認します。
7. データベース インスタンスを作成する
このタスクでは、前のタスクで作成した Entity と DAO を使用する RoomDatabase を作成します。データベース クラスは、エンティティのリストとデータ アクセス オブジェクトを定義します。基礎的な接続のメイン アクセス ポイントでもあります。
Database クラスは、定義した DAO のインスタンスをアプリに提供します。アプリはこの DAO を使用して、関連するデータ エンティティ オブジェクトのインスタンスとしてデータベースからデータを取得できます。また、定義されたデータ エンティティを使用して、対応するテーブルの行を更新したり、挿入用の新しい行を作成したりできます。
@Database アノテーションを付けた抽象 RoomDatabase クラスを作成する必要があります。このクラスには、RoomDatabase のインスタンスが存在しない場合は作成し、RoomDatabase のインスタンスが存在する場合はそれを返す 1 つのメソッドがあります。
RoomDatabase インスタンスを取得する一般的なプロセスは次のとおりです。
- RoomDatabaseを拡張する- public abstractクラスを作成します。定義した新しい抽象クラスは、データベース ホルダーとして機能します。- Roomが実装を作成するため、定義したクラスは抽象クラスです。
- クラスに @Databaseアノテーションを付けます。引数で、データベースのエンティティをリストしてバージョン番号を設定します。
- ItemDaoインスタンスを返す抽象メソッドまたはプロパティを定義すると、- Roomが実装を生成します。
- アプリ全体で必要な RoomDatabaseのインスタンスは 1 つのみであるため、RoomDatabaseをシングルトンにします。
- Roomの- Room.databaseBuilderを使用して、存在しない場合にのみ(- item_database)データベースを作成します。それ以外の場合は、既存のデータベースを返します。
データベースを作成する
- dataパッケージで、Kotlin クラス- ItemRoomDatabase.ktを作成します。
- ItemRoomDatabase.ktファイルで、- RoomDatabaseを拡張する- abstractクラスとして、- ItemRoomDatabaseクラスを作成します。クラスに- @Databaseアノテーションを付けます。次のステップでパラメータ欠落エラーを修正します。
@Database
abstract class ItemRoomDatabase : RoomDatabase() {}
- @Databaseアノテーションには、- Roomがデータベースを構築できるように、複数の引数が必要です。
- entitiesのリストを持つ唯一のクラスとして- Itemを指定します。
- versionを- 1に設定します。データベース テーブルのスキーマを変更するたびに、バージョン番号を増やす必要があります。
- スキーマのバージョン履歴のバックアップを保持しないように、exportSchemaをfalseに設定します。
@Database(entities = [Item::class], version = 1, exportSchema = false)
- データベースは DAO について知る必要があります。クラスの本文内で、ItemDaoを返す抽象関数を宣言します。複数の DAO を持つことができます。
abstract fun itemDao(): ItemDao
- 抽象関数の下で、companionオブジェクトを定義します。コンパニオン オブジェクトは、クラス名を修飾子として使用し、データベースを作成または取得するためのメソッドにアクセスできるようにします。
 companion object {}
- companionオブジェクト内で、データベース用に null 許容のプライベート変数- INSTANCEを宣言し、- nullに初期化します。- INSTANCE変数は、データベースの作成時に、データベースに対する参照を保持します。これは、ある時点で開かれているデータベースのインスタンス(作成と維持にコストのかかるリソース)を 1 つだけ維持する際に役立ちます。
INSTANCE に @Volatile アノテーションを付けます。volatile 変数の値はキャッシュに保存されません。書き込みと読み取りはすべてメインメモリとの間で行われます。これにより、INSTANCE の値が常に最新になり、すべての実行スレッドで同じになります。つまり、あるスレッドが INSTANCE に加えた変更が、すぐに他のすべてのスレッドに反映されます。
@Volatile
private var INSTANCE: ItemRoomDatabase? = null
- INSTANCEの下、- companionオブジェクト内で、データベース ビルダーに必要な- Contextパラメータを持つ- getDatabase()メソッドを定義します。- ItemRoomDatabase型を返します。- getDatabase()はまだ何も返していないため、エラーが表示されます。
fun getDatabase(context: Context): ItemRoomDatabase {}
- 複数のスレッドが競合状態になってデータベース インスタンスを同時に要求し、結果的に 1 つではなく 2 つのデータベースが作成される可能性があります。データベースを取得するコードを synchronizedブロックで囲むと、このコードブロックには一度に 1 つのスレッドしか入ることができず、データベースは一度だけ初期化されます。
getDatabase() 内で INSTANCE 変数を返すか、INSTANCE が null の場合は synchronized{} ブロック内で初期化します。これにはエルビス演算子(?:)を使用します。関数ブロック内でロックするコンパニオン オブジェクト this を渡します。エラーの修正はこの後のステップで行います。
return INSTANCE ?: synchronized(this) { }
- synchronized ブロック内で、valインスタンス変数を作成し、データベース ビルダーを使用してデータベースを取得します。エラーがまだ残っていますが、これは次のステップで修正します。
val instance = Room.databaseBuilder()
- synchronizedブロックの最後で- instanceを返します。
return instance
- synchronizedブロック内で、- instance変数を初期化し、データベース ビルダーを使用してデータベースを取得します。アプリ コンテキスト、データベース クラス、データベースの名前- item_databaseを- Room.databaseBuilder()に渡します。
val instance = Room.databaseBuilder(
   context.applicationContext,
   ItemRoomDatabase::class.java,
   "item_database"
)
Android Studio は、型の不一致エラーを生成します。このエラーを解消するには、以降のステップで移行戦略と build() を追加する必要があります。
- 必要な移行戦略をビルダーに追加します。.fallbackToDestructiveMigration()を使用します。
通常は、スキーマが変更されたときの移行戦略を移行オブジェクトに指定する必要があります。「移行オブジェクト」とは、データが失われないように、古いスキーマの行をすべて取得して新しいスキーマの行に変換する方法を定義するオブジェクトです。移行は、この Codelab の対象外です。簡単なソリューションは、データベースを破棄して再構築することです。この場合データは失われます。
.fallbackToDestructiveMigration()
- データベース インスタンスを作成するために、.build()を呼び出します。これで Android Studio のエラーが削除されます。
.build()
- synchronizedブロック内で- INSTANCE = instanceを割り当てます。
INSTANCE = instance
- synchronizedブロックの最後で- instanceを返します。最終的なコードは次のようなります。
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(entities = [Item::class], version = 1, exportSchema = false)
abstract class ItemRoomDatabase : RoomDatabase() {
   abstract fun itemDao(): ItemDao
   companion object {
       @Volatile
       private var INSTANCE: ItemRoomDatabase? = null
       fun getDatabase(context: Context): ItemRoomDatabase {
           return INSTANCE ?: synchronized(this) {
               val instance = Room.databaseBuilder(
                   context.applicationContext,
                   ItemRoomDatabase::class.java,
                   "item_database"
               )
                   .fallbackToDestructiveMigration()
                   .build()
               INSTANCE = instance
               return instance
           }
       }
   }
}
- コードをビルドして、エラーがないことを確認します。
Application クラスを実装する
このタスクでは、Application クラスでデータベース インスタンスをインスタンス化します。
- InventoryApplication.ktを開き、- ItemRoomDatabase型の- databaseという- valを作成します。- ItemRoomDatabaseに対して- getDatabase()を呼び出してコンテキストを渡し、- databaseインスタンスをインスタンス化します。- lazyデリゲートを使用して、(アプリの起動時ではなく)参照が最初に必要になった(アクセスされた)ときにインスタンス- databaseが作成されるようにします。これにより、最初のアクセス時にデータベース(ディスク上の物理データベース)が作成されます。
import android.app.Application
import com.example.inventory.data.ItemRoomDatabase
class InventoryApplication : Application(){
   val database: ItemRoomDatabase by lazy { ItemRoomDatabase.getDatabase(this) }
}
database インスタンスは、この Codelab で後ほど ViewModel インスタンスを作成するときに使用します。
これで、Room を扱うためのビルディング ブロックが揃いました。このコードはコンパイルされて実行されますが、実際に機能するかどうかを確認する方法はありません。この機会に、Inventory データベースに新しいアイテムを追加して、データベースをテストしてみましょう。そのためには、データベースと通信するための ViewModel が必要です。
8. ViewModel を追加する
これまでに、データベースを作成し、UI クラスはスターター コードに含まれていました。アプリの一時的なデータを保存し、データベースにアクセスするには、ViewModel が必要です。Inventory ViewModel は DAO を介してデータベースを操作し、UI にデータを提供します。データベース操作はすべてメイン UI スレッドから切り離す必要があるため、コルーチンと viewModelScope を使用します。

Inventory ViewModel を作成する
- com.example.inventoryパッケージで、Kotlin クラスファイル- InventoryViewModel.ktを作成します。
- InventoryViewModelクラスを- ViewModelクラスから拡張します。- ItemDaoオブジェクトをパラメータとしてデフォルト コンストラクタに渡します。
class InventoryViewModel(private val itemDao: ItemDao) : ViewModel() {}
- クラス外の InventoryViewModel.ktファイルの最後にInventoryViewModelFactoryクラスを追加して、InventoryViewModelインスタンスをインスタンス化します。ItemDaoインスタンスであるInventoryViewModelと同じコンストラクタ パラメータを渡します。クラスをViewModelProvider.Factoryクラスから拡張します。実装されていないメソッドに関するエラーは次のステップで修正します。
class InventoryViewModelFactory(private val itemDao: ItemDao) : ViewModelProvider.Factory {
}
- 赤い電球をクリックして [Implement Members] を選択するか、次のように ViewModelProvider.Factoryクラス内のcreate()メソッドをオーバーライドし、任意のクラス型を引数に取ってViewModelオブジェクトを返します。
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
   TODO("Not yet implemented")
}
- create()メソッドを実装します。- modelClassが- InventoryViewModelクラスと同じであることを確認してから、そのインスタンスを返します。そうでない場合は、例外をスローします。
if (modelClass.isAssignableFrom(InventoryViewModel::class.java)) {
   @Suppress("UNCHECKED_CAST")
   return InventoryViewModel(itemDao) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
ViewModel にデータを入力する
このタスクでは、InventoryViewModel クラスにデータを入力して、インベントリ データをデータベースに追加します。Inventory アプリで Item エンティティと [Add Item] 画面を確認します。
@Entity
data class Item(
   @PrimaryKey(autoGenerate = true)
   val id: Int = 0,
   @ColumnInfo(name = "name")
   val itemName: String,
   @ColumnInfo(name = "price")
   val itemPrice: Double,
   @ColumnInfo(name = "quantity")
   val quantityInStock: Int
)

エンティティをデータベースに追加するには、対象アイテムの名前、価格、在庫数の情報が必要です。Codelab で後ほど [Add Item] 画面を使用して、これらの詳細をユーザーから取得します。現在のタスクでは、ViewModel への入力として 3 つの文字列を使用して、Item エンティティ インスタンスに変換し、ItemDao インスタンスを使用してデータベースに保存します。それでは実装しましょう。
- InventoryViewModelクラスで、- insertItem()という- private関数を追加します。これは- Itemオブジェクトを受け取り、データを非ブロック形式でデータベースに追加します。
private fun insertItem(item: Item) {
}
- メインスレッド以外でデータベースを操作するには、コルーチンを開始し、その中で DAO メソッドを呼び出します。insertItem()メソッド内で、viewModelScope.launchを使用してViewModelScope内のコルーチンを開始します。launch 関数内で、itemDaoに対して suspend 関数insert()を呼び出してitemを渡します。ViewModelScopeはViewModelクラスの拡張プロパティであり、ViewModelが破棄されると、子コルーチンを自動的にキャンセルします。
private fun insertItem(item: Item) {
   viewModelScope.launch {
       itemDao.insert(item)
   }
}
kotlinx.coroutines.launch, androidx.lifecycle.viewModelScope
com.example.inventory.data.Item をインポートします(自動的にインポートされない場合)。
- InventoryViewModelクラスで、3 つの文字列を受け取り- Itemインスタンスを返す別のプライベート関数を追加します。
private fun getNewItemEntry(itemName: String, itemPrice: String, itemCount: String): Item {
   return Item(
       itemName = itemName,
       itemPrice = itemPrice.toDouble(),
       quantityInStock = itemCount.toInt()
   )
}
- さらに InventoryViewModelクラス内で、3 つの文字列を受け取りアイテムの詳細を取得するパブリック関数addNewItem()を追加します。getNewItemEntry()関数にアイテム詳細の文字列を渡し、返された値をnewItemという val に割り当てます。insertItem()を呼び出してnewItemを渡し、新しいエンティティをデータベースに追加します。これは、アイテムの詳細をデータベースに追加するために、UI フラグメントから呼び出されます。
fun addNewItem(itemName: String, itemPrice: String, itemCount: String) {
   val newItem = getNewItemEntry(itemName, itemPrice, itemCount)
   insertItem(newItem)
}
addNewItem() では viewModelScope.launch が使用されていませんが、上の insertItem() では DAO メソッドを呼び出すときに必要となります。これは、「suspend 関数はコルーチンや他の suspend 関数からしか呼び出せない」ためです。関数 itemDao.insert(item) は suspend 関数です。
エンティティをデータベースに追加するために必要な関数がすべて追加されました。次のタスクでは、以上の関数を使用するように Add Item フラグメントを更新します。
9. AddItemFragment を更新する
- AddItemFragment.ktで、- AddItemFragmentクラスの先頭に、- InventoryViewModel型の- viewModelという- private valを作成します。Kotlin プロパティのデリゲート- by activityViewModels()を使用して、フラグメント間で- ViewModelを共有します。エラーは次のステップで修正します。
private val viewModel: InventoryViewModel by activityViewModels {
}
- ラムダ内で、InventoryViewModelFactory()コンストラクタを呼び出し、ItemDaoインスタンスを渡します。以前のタスクで作成したdatabaseインスタンスを使用して、itemDaoコンストラクタを呼び出します。
private val viewModel: InventoryViewModel by activityViewModels {
   InventoryViewModelFactory(
       (activity?.application as InventoryApplication).database
           .itemDao()
   )
}
- viewModel定義の下で、- Item型の- itemという- lateinit varを作成します。
 lateinit var item: Item
- [Add Item] 画面には、ユーザーからアイテムの詳細を取得するためのテキスト フィールドが 3 つあります。このステップでは、テキスト フィールドのテキストが空でないかどうかを検証する関数を追加します。この関数を使用してユーザー入力を検証してから、データベースのエンティティを追加または更新します。この検証は、フラグメントではなく ViewModelで行う必要があります。InventoryViewModelクラスで、isEntryValid()という次のpublic関数を追加します。
fun isEntryValid(itemName: String, itemPrice: String, itemCount: String): Boolean {
   if (itemName.isBlank() || itemPrice.isBlank() || itemCount.isBlank()) {
       return false
   }
   return true
}
- AddItemFragment.ktで、- onCreateView()関数の下に、- Booleanを返す- isEntryValid()という- private関数を作成します。次のステップで戻り値欠落エラーを修正します。
private fun isEntryValid(): Boolean {
}
- AddItemFragmentクラスで、- isEntryValid()関数を実装します。- viewModelインスタンスに対して- isEntryValid()関数を呼び出し、テキストビューからのテキストを渡します。- viewModel.isEntryValid()関数の値を返します。
private fun isEntryValid(): Boolean {
   return viewModel.isEntryValid(
       binding.itemName.text.toString(),
       binding.itemPrice.text.toString(),
       binding.itemCount.text.toString()
   )
}
- isEntryValid()関数の下の- AddItemFragmentクラスで、パラメータなしで- addNewItem()という別の- private関数を追加し、何も返しません。関数内で、- if条件内で- isEntryValid()を呼び出します。
private fun addNewItem() {
   if (isEntryValid()) {
   }
}
- ifブロック内で、- viewModelインスタンスに対して- addNewItem()メソッドを呼び出します。ユーザーが入力したアイテムの詳細を渡し、- bindingインスタンスを使用して読み取ります。
if (isEntryValid()) {
   viewModel.addNewItem(
   binding.itemName.text.toString(),
   binding.itemPrice.text.toString(),
   binding.itemCount.text.toString(),
   )
}
- ifブロックの下で、- val- actionを作成して- ItemListFragmentに戻ります。- findNavController- ().navigate()を呼び出して- actionを渡します。
val action = AddItemFragmentDirections.actionAddItemFragmentToItemListFragment()
findNavController().navigate(action)
androidx.navigation.fragment.findNavController. をインポートします。
- 完成したメソッドは次のようになります。
private fun addNewItem() {
       if (isEntryValid()) {
           viewModel.addNewItem(
               binding.itemName.text.toString(),
               binding.itemPrice.text.toString(),
               binding.itemCount.text.toString(),
           )
           val action = AddItemFragmentDirections.actionAddItemFragmentToItemListFragment()
           findNavController().navigate(action)
       }
}
- すべてをまとめるために、[SAVE] ボタンにクリック ハンドラを追加します。AddItemFragmentクラスの、onDestroyView()関数の上で、onViewCreated()関数をオーバーライドします。
- onViewCreated()関数内で、保存ボタンにクリック ハンドラを追加し、そこから- addNewItem()を呼び出します。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   super.onViewCreated(view, savedInstanceState)
   binding.saveAction.setOnClickListener {
       addNewItem()
   }
}
- アプリをビルドして実行します。[+] FAB をタップします。[Add Item] 画面でアイテムの詳細を追加して [SAVE] をタップします。この操作を行うとデータは保存されますが、アプリにはまだ何も表示されません。次のタスクでは Database Inspector を使用して、保存したデータを表示します。

Database Inspector を使用してデータベースを表示する
- API レベル 26 以降を搭載した接続済みデバイスまたはエミュレータでアプリを実行します。Database Inspector は、API レベル 26 を搭載したエミュレータやデバイスで最適に機能します。
- Android Studio で、メニューバーから [View] > [Tool Windows] > [Database Inspector] を選択します。
- [Database Inspector] ペインで、プルダウン メニューから [com.example.inventory] を選択します。
- Inventory アプリの item_database が [Databases] ペインに表示されます。item_database のノードを開き、[Item] を選択して検査します。[Databases] ペインが空の場合はエミュレータを使用し、[Add Item] 画面からデータベースにアイテムを追加します。
- Database Inspector の [Live updates] チェックボックスをオンにすると、エミュレータまたはデバイス上で実行中のアプリを操作したときに、表示されるデータが自動的に更新されます。

お疲れさまでした。Room を使用してデータを永続化するアプリを作成しました。次の Codelab では、アプリに RecyclerView を追加してデータベース上のアイテムを表示し、エンティティの削除や更新などの新機能をアプリに追加します。ご参加をお待ちしております。
10. 解答コード
この 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] ツール ウィンドウでプロジェクト ファイルを見て、アプリがどのように設定されているかを確認します。
11. 概要
- テーブルを、@Entityアノテーション付きのデータクラスとして定義する。@ColumnInfoアノテーション付きのプロパティを、テーブルの列として定義する。
- データ アクセス オブジェクト(DAO)を、@Daoアノテーション付きのインターフェースとして定義する。DAO は、Kotlin 関数をデータベース クエリにマッピングする。
- アノテーションを使用して、@Insert、@Delete、@Update関数を定義する。
- SQLite クエリ文字列の @Queryアノテーションを、他のクエリのパラメータとして使用する。
- Database Inspector を使用して、Android SQLite データベースに保存されているデータを表示する。
12. 詳細
Android デベロッパー ドキュメント
ブログ投稿
動画
その他のドキュメントと記事
