Room を使用してデータを永続化する

1. 始める前に

製品版品質のアプリには通常、ユーザーがアプリを閉じた後も保存しておく必要のあるデータがあります。たとえば、曲の再生リスト、To-Do リストの項目、収支の記録、星座表、個人データの履歴などが挙げられます。ほとんどの場合、この永続データの保存にはデータベースが使用されます。

Room は、Android Jetpack の一部である永続ライブラリで、SQLite データベースの上に位置する抽象化レイヤです。SQLite は専門言語(SQL)を使用してデータベース操作を行います。SQLite を直接使用する代わりに Room を使用すると、データベースのセットアップ、設定、操作が簡単になります。Room には、SQLite ステートメントのコンパイル時チェック機能もあります。

下図に、このコースで推奨されているアーキテクチャ全体における Room の位置付けを示します。

7521165e051cc0d4.png

前提条件

  • Android アプリの基本的なユーザー インターフェース(UI)の作成方法を理解している。
  • アクティビティ、フラグメント、ビューの使用方法を理解している。
  • フラグメント間を移動する方法、フラグメント間でデータを渡すために Safe Args を使用する方法を理解している。
  • Android アーキテクチャ コンポーネントの ViewModelLiveDataFlow に精通しており、ViewModelProvider.Factory を使用して ViewModel をインスタンス化する方法を理解している。
  • 同時実行の基本に精通している。
  • 長時間実行タスクにコルーチンを使用する方法を理解している。
  • SQL データベースと SQLite 言語に関する基礎知識がある。

学習内容

  • Room ライブラリを使用して SQLite データベースを作成し、操作する方法。
  • エンティティ クラス、DAO クラス、データベース クラスを作成する方法。
  • データ アクセス オブジェクト(DAO)を使用して Kotlin 関数を SQL クエリにマッピングする方法。

作成するアプリの概要

  • インベントリ アイテムを SQLite データベースに保存する Inventory アプリを作成します。

必要なもの

  • Inventory アプリのスターター コード。
  • Android Studio がインストールされているパソコン

2. アプリの概要

この Codelab では、Inventory アプリというスターター アプリを扱い、Room ライブラリを使用してアプリにデータベース レイヤを追加します。アプリの最終バージョンでは、RecyclerView を使用してインベントリ データベースからリストアイテムを表示します。ユーザーは、新しいアイテムの追加、既存のアイテムの更新、インベントリ データベースからのアイテムの削除ができます(アプリの機能は次回の Codelab で完成します)。

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

439ad9a8183278c5.png

3.スターター アプリの概要

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

この Codelab では、ここで学んだ機能を使って拡張するためのスターター コードが提供されます。スターター コードには、以前の Codelab で学んだコードだけでなく、今後の Codelab で学ぶ予定の、見慣れないコードが含まれていることもあります。

GitHub のスターター コードを使用する場合、フォルダ名は android-basics-kotlin-inventory-app-starter です。Android Studio でプロジェクトを開くときは、このフォルダを選択してください。

この Codelab のコードを取得して Android Studio で開く手順は次のとおりです。

コードを取得する

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

5b0a76c50478a73f.png

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

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

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

36cc44fcf0f89a1d.png

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

21f3eec988dcfbe9.png

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

スターター コードの概要

  1. Android Studio でスターター コードのプロジェクトを開きます。
  2. Android デバイスまたはエミュレータでアプリを実行します。エミュレータまたは接続済みのデバイスが API レベル 26 以降を搭載していることを確認します。Database Inspector は、API レベル 26 を搭載したエミュレータやデバイスで最適に機能します。
  3. アプリにインベントリ データは表示されません。データベースに新しいアイテムを追加する FAB に注目します。
  4. FAB をクリックします。自動で新しい画面に移動し、新しいアイテムの詳細情報を入力できるようになります。

9c5e361a89453821.png

スターター コードの問題点

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

f0931dab5089a14f.png

この 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 のコンポーネントが連携してデータベースを操作する仕組みを示します。

33a193a68c9a8e0e.png

Room ライブラリを追加する

このタスクでは、必要な Room コンポーネント ライブラリを Gradle ファイルに追加します。

  1. モジュール レベルの 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 に伝えるためのマッピングがあります。今回のアプリでは、エンティティはアイテム名、アイテム価格、利用可能な在庫など、インベントリ アイテムに関する情報を保持します。

8c9f1659ee82ca43.png

@Entity アノテーションは、クラスをデータベースの Entity クラスとしてマークします。アイテムを保持するためのデータベース テーブルが Entity クラスごとに作成されます。Entity の各フィールドは、特に明記されていない限り、データベースの列として表されます(詳細については Entity のドキュメントをご覧ください)。データベースに格納されるすべてのエンティティ インスタンスに主キーが必要です。主キーは、データベース テーブルのすべてのレコードやエントリを一意に識別するために使用します。一度割り当てた主キーは変更できず、データベース内に存在する限り、エンティティ オブジェクトを表します。

このタスクでは、Entity クラスを作成します。アイテムごとに次のインベントリ情報を格納するフィールドを定義します。

  • 主キーを格納する Int
  • アイテム名を格納する String
  • アイテム価格を格納する double
  • 在庫数を格納する Int
  1. Android Studio でスターター コードを開きます。
  2. com.example.inventory 基本パッケージの下に data というパッケージを作成します。

be39b42484ba2664.png

  1. data パッケージ内に Item という Kotlin クラスを作成します。このクラスは、アプリ内のデータベース エンティティを表します。次のステップでは、インベントリ情報を格納するために対応するフィールドを追加します。
  2. 次のコードを使用して Item クラスの定義を更新します。プライマリ コンストラクタのパラメータとして、Int 型の idString, 型の itemNameDouble 型の itemPriceInt 型の 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 としてマークする必要があります。
  • データクラスを abstractopensealedinner にすることはできません。

データクラスの詳細については、こちらのドキュメントをご覧ください。

  1. クラス定義の前に data キーワードを付けて、Item クラスをデータクラスに変換します。
data class Item(
   val id: Int = 0,
   val itemName: String,
   val itemPrice: Double,
   val quantityInStock: Int
)
  1. Item クラス宣言の上で、データクラスに @Entity アノテーションを付けます。tableName 引数を使用して、item を SQLite テーブル名として指定します。
@Entity(tableName = "item")
data class Item(
   ...
)
  1. id を主キーとして識別するには、id プロパティに @PrimaryKey アノテーションを付けます。Room が各エンティティの ID を生成するように、パラメータ autoGeneratetrue に設定します。これにより、各アイテムの ID が一意になります。
@Entity(tableName = "item")
data class Item(
   @PrimaryKey(autoGenerate = true)
   val id: Int = 0,
   ...
)
  1. 残りのプロパティに @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 の機能は、基となる永続化レイヤでのデータベース操作に関連するすべての複雑さを、アプリの残りの部分から隠すことです。これにより、データを使用するコードから独立してデータアクセス レイヤを変更できます。

7a8480711f04b3ef.png

このタスクでは、Room のデータ アクセス オブジェクト(DAO)を定義します。データ アクセス オブジェクトは、データベースにアクセスするインターフェースを定義する Room の主要コンポーネントです。

作成する DAO は、データベースに対してクエリ(取得)、挿入、削除、更新を行うための便利なメソッドを提供するカスタム インターフェースになります。Room はコンパイル時にこのクラスの実装を生成します。

一般的なデータベース操作の場合、Room ライブラリには @Insert@Delete@Update などの便利なアノテーションが用意されています。それ以外の場合は、@Query アノテーションがあります。SQLite でサポートされている、あらゆるクエリを記述できます。

さらに、Android Studio でクエリを記述すると、コンパイラが SQL クエリの構文エラーをチェックします。

このインベントリ アプリの場合、次のことを行える必要があります。

  • 新しいアイテムの挿入または追加。
  • 既存のアイテムを更新して、名前、価格、数量を更新する。
  • 主キーである id に基づいて、特定のアイテムを取得する。
  • すべてのアイテムを取得して、表示できるようにする。
  • データベースのエントリを削除する。

bb381857d5fba511.png

それでは、アプリにアイテム DAO を実装します。

  1. data パッケージで、Kotlin クラス ItemDao.kt を作成します。
  2. クラス定義を interface に変更し、@Dao アノテーションを付けます。
@Dao
interface ItemDao {
}
  1. インターフェースの本文内に @Insert アノテーションを追加します。@Insert の下に、Entity クラスの item のインスタンスを引数として取る insert() 関数を追加します。データベース操作は実行に時間がかかる可能性があるため、別のスレッドで実行する必要があります。関数を suspend 関数にして、コルーチンから呼び出せるようにします。
@Insert
suspend fun insert(item: Item)
  1. 引数 OnConflict を追加し、値 OnConflictStrategy.IGNORE を割り当てます。引数 OnConflict は、競合が発生した場合の処理を Room に伝えます。OnConflictStrategy.IGNORE 戦略は、主キーがデータベースにすでに存在する場合、新しいアイテムを無視します。利用可能な競合戦略について詳しくは、こちらのドキュメントをご覧ください。
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(item: Item)

これで、item をデータベースに挿入するために必要なすべてのコードが Room によって生成されるようになりました。Kotlin コードから insert() を呼び出すと、Room が SQL クエリを実行し、エンティティをデータベースに挿入します(注: 関数には任意の名前を付けることができます。insert() である必要はありません)。

  1. 1 つの item 用に update() 関数と @Update アノテーションを追加します。渡されるエンティティと同じキーを持つエンティティが更新されます。エンティティの他のプロパティの一部または全部を更新できます。insert() メソッドと同様に、次の update() メソッドを suspend にします。
@Update
suspend fun update(item: Item)
  1. アイテムを削除するために、@Delete アノテーションと delete() 関数を追加します。サスペンド メソッドにします。@Delete アノテーションは、1 つまたは複数のアイテムを削除します(注: 削除するエンティティを渡す必要があります。エンティティがない場合は、delete() 関数を呼び出す前に取得する必要があります)。
@Delete
suspend fun delete(item: Item)

残りの機能には便利なアノテーションがないため、@Query アノテーションを使用して SQLite クエリを指定する必要があります。

  1. 指定した id に基づいてアイテム テーブルから特定のアイテムを取得する SQLite クエリを記述します。その後、Room アノテーションを追加して、後のステップで次のクエリの修正版を使用します。次のステップでは、Room を使用してこれを DAO メソッドに変更します。
  2. item からすべての列を選択します。
  3. WHERE 句で id を特定の値と一致させます。

例:

SELECT * from item WHERE id = 1
  1. 上の SQL クエリを、Room アノテーションと引数で使用するように変更します。@Query アノテーションを追加し、クエリを文字列パラメータとして @Query アノテーションに指定します。アイテム テーブルからアイテムを取得する SQLite クエリを、String パラメータとして @Query に追加します。
  2. item からすべての列を選択します。
  3. WHERE 句で id を :id 引数と一致させます。:id に注目してください。クエリ内でコロン表記を使用して、関数内の引数を参照しています。
@Query("SELECT * from item WHERE id = :id")
  1. @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 をインポートする必要があります。

  1. @QuerygetItems() 関数を追加します。
  2. SQLite クエリが item テーブルのすべての列を昇順で返すようにします。
  3. getItems()Item エンティティのリストを Flow として返すようにします。Room がこの Flow を最新の状態に維持します。つまり、データを明示的に取得する必要があるのは一度だけです。
@Query("SELECT * from item ORDER BY name ASC")
fun getItems(): Flow<List<Item>>
  1. 目に見える変化はありませんが、アプリを実行してエラーがないことを確認します。

7. データベース インスタンスを作成する

このタスクでは、前のタスクで作成した Entity と DAO を使用する RoomDatabase を作成します。データベース クラスは、エンティティのリストとデータ アクセス オブジェクトを定義します。基礎的な接続のメイン アクセス ポイントでもあります。

Database クラスは、定義した DAO のインスタンスをアプリに提供します。アプリはこの DAO を使用して、関連するデータ エンティティ オブジェクトのインスタンスとしてデータベースからデータを取得できます。また、定義されたデータ エンティティを使用して、対応するテーブルの行を更新したり、挿入用の新しい行を作成したりできます。

@Database アノテーションを付けた抽象 RoomDatabase クラスを作成する必要があります。このクラスには、RoomDatabase のインスタンスが存在しない場合は作成し、RoomDatabase のインスタンスが存在する場合はそれを返す 1 つのメソッドがあります。

RoomDatabase インスタンスを取得する一般的なプロセスは次のとおりです。

  • RoomDatabase を拡張する public abstract クラスを作成します。定義した新しい抽象クラスは、データベース ホルダーとして機能します。Room が実装を作成するため、定義したクラスは抽象クラスです。
  • クラスに @Database アノテーションを付けます。引数で、データベースのエンティティをリストしてバージョン番号を設定します。
  • ItemDao インスタンスを返す抽象メソッドまたはプロパティを定義すると、Room が実装を生成します。
  • アプリ全体で必要な RoomDatabase のインスタンスは 1 つのみであるため、RoomDatabase をシングルトンにします。
  • RoomRoom.databaseBuilder を使用して、存在しない場合にのみ(item_database)データベースを作成します。それ以外の場合は、既存のデータベースを返します。

データベースを作成する

  1. data パッケージで、Kotlin クラス ItemRoomDatabase.kt を作成します。
  2. ItemRoomDatabase.kt ファイルで、RoomDatabase を拡張する abstract クラスとして、ItemRoomDatabase クラスを作成します。クラスに @Database アノテーションを付けます。次のステップでパラメータ欠落エラーを修正します。
@Database
abstract class ItemRoomDatabase : RoomDatabase() {}
  1. @Database アノテーションには、Room がデータベースを構築できるように、複数の引数が必要です。
  • entities のリストを持つ唯一のクラスとして Item を指定します。
  • version1 に設定します。データベース テーブルのスキーマを変更するたびに、バージョン番号を増やす必要があります。
  • スキーマのバージョン履歴のバックアップを保持しないように、exportSchemafalse に設定します。
@Database(entities = [Item::class], version = 1, exportSchema = false)
  1. データベースは DAO について知る必要があります。クラスの本文内で、ItemDao を返す抽象関数を宣言します。複数の DAO を持つことができます。
abstract fun itemDao(): ItemDao
  1. 抽象関数の下で、companion オブジェクトを定義します。コンパニオン オブジェクトは、クラス名を修飾子として使用し、データベースを作成または取得するためのメソッドにアクセスできるようにします。
 companion object {}
  1. companion オブジェクト内で、データベース用に null 許容のプライベート変数 INSTANCE を宣言し、null に初期化します。INSTANCE 変数は、データベースの作成時に、データベースに対する参照を保持します。これは、ある時点で開かれているデータベースのインスタンス(作成と維持にコストのかかるリソース)を 1 つだけ維持する際に役立ちます。

INSTANCE@Volatile アノテーションを付けます。volatile 変数の値はキャッシュに保存されません。書き込みと読み取りはすべてメインメモリとの間で行われます。これにより、INSTANCE の値が常に最新になり、すべての実行スレッドで同じになります。つまり、あるスレッドが INSTANCE に加えた変更が、すぐに他のすべてのスレッドに反映されます。

@Volatile
private var INSTANCE: ItemRoomDatabase? = null
  1. INSTANCE の下、companion オブジェクト内で、データベース ビルダーに必要な Context パラメータを持つ getDatabase() メソッドを定義します。ItemRoomDatabase 型を返します。getDatabase() はまだ何も返していないため、エラーが表示されます。
fun getDatabase(context: Context): ItemRoomDatabase {}
  1. 複数のスレッドが競合状態になってデータベース インスタンスを同時に要求し、結果的に 1 つではなく 2 つのデータベースが作成される可能性があります。データベースを取得するコードを synchronized ブロックで囲むと、このコードブロックには一度に 1 つのスレッドしか入ることができず、データベースは一度だけ初期化されます。

getDatabase() 内で INSTANCE 変数を返すか、INSTANCE が null の場合は synchronized{} ブロック内で初期化します。これにはエルビス演算子(?:)を使用します。関数ブロック内でロックするコンパニオン オブジェクト this を渡します。エラーの修正はこの後のステップで行います。

return INSTANCE ?: synchronized(this) { }
  1. synchronized ブロック内で、val インスタンス変数を作成し、データベース ビルダーを使用してデータベースを取得します。エラーがまだ残っていますが、これは次のステップで修正します。
val instance = Room.databaseBuilder()
  1. synchronized ブロックの最後で instance を返します。
return instance
  1. synchronized ブロック内で、instance 変数を初期化し、データベース ビルダーを使用してデータベースを取得します。アプリ コンテキスト、データベース クラス、データベースの名前 item_databaseRoom.databaseBuilder() に渡します。
val instance = Room.databaseBuilder(
   context.applicationContext,
   ItemRoomDatabase::class.java,
   "item_database"
)

Android Studio は、型の不一致エラーを生成します。このエラーを解消するには、以降のステップで移行戦略と build() を追加する必要があります。

  1. 必要な移行戦略をビルダーに追加します。.fallbackToDestructiveMigration() を使用します。

通常は、スキーマが変更されたときの移行戦略を移行オブジェクトに指定する必要があります。「移行オブジェクト」とは、データが失われないように、古いスキーマの行をすべて取得して新しいスキーマの行に変換する方法を定義するオブジェクトです。移行は、この Codelab の対象外です。簡単なソリューションは、データベースを破棄して再構築することです。この場合データは失われます。

.fallbackToDestructiveMigration()
  1. データベース インスタンスを作成するために、.build() を呼び出します。これで Android Studio のエラーが削除されます。
.build()
  1. synchronized ブロック内で INSTANCE = instance を割り当てます。
INSTANCE = instance
  1. 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
           }
       }
   }
}
  1. コードをビルドして、エラーがないことを確認します。

Application クラスを実装する

このタスクでは、Application クラスでデータベース インスタンスをインスタンス化します。

  1. 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 を使用します。

91298a7c05e4f5e0.png

Inventory ViewModel を作成する

  1. com.example.inventory パッケージで、Kotlin クラスファイル InventoryViewModel.kt を作成します。
  2. InventoryViewModel クラスを ViewModel クラスから拡張します。ItemDao オブジェクトをパラメータとしてデフォルト コンストラクタに渡します。
class InventoryViewModel(private val itemDao: ItemDao) : ViewModel() {}
  1. クラス外の InventoryViewModel.kt ファイルの最後に InventoryViewModelFactory クラスを追加して、InventoryViewModel インスタンスをインスタンス化します。ItemDao インスタンスである InventoryViewModel と同じコンストラクタ パラメータを渡します。クラスを ViewModelProvider.Factory クラスから拡張します。実装されていないメソッドに関するエラーは次のステップで修正します。
class InventoryViewModelFactory(private val itemDao: ItemDao) : ViewModelProvider.Factory {
}
  1. 赤い電球をクリックして [Implement Members] を選択するか、次のように ViewModelProvider.Factory クラス内の create() メソッドをオーバーライドし、任意のクラス型を引数に取って ViewModel オブジェクトを返します。
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
   TODO("Not yet implemented")
}
  1. create() メソッドを実装します。modelClassInventoryViewModel クラスと同じであることを確認してから、そのインスタンスを返します。そうでない場合は、例外をスローします。
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
)

85c644aced4198c5.png

エンティティをデータベースに追加するには、対象アイテムの名前、価格、在庫数の情報が必要です。Codelab で後ほど [Add Item] 画面を使用して、これらの詳細をユーザーから取得します。現在のタスクでは、ViewModel への入力として 3 つの文字列を使用して、Item エンティティ インスタンスに変換し、ItemDao インスタンスを使用してデータベースに保存します。それでは実装しましょう。

  1. InventoryViewModel クラスで、insertItem() という private 関数を追加します。これは Item オブジェクトを受け取り、データを非ブロック形式でデータベースに追加します。
private fun insertItem(item: Item) {
}
  1. メインスレッド以外でデータベースを操作するには、コルーチンを開始し、その中で DAO メソッドを呼び出します。insertItem() メソッド内で、viewModelScope.launch を使用して ViewModelScope 内のコルーチンを開始します。launch 関数内で、itemDao に対して suspend 関数 insert() を呼び出して item を渡します。ViewModelScopeViewModel クラスの拡張プロパティであり、ViewModel が破棄されると、子コルーチンを自動的にキャンセルします。
private fun insertItem(item: Item) {
   viewModelScope.launch {
       itemDao.insert(item)
   }
}

kotlinx.coroutines.launch, androidx.lifecycle.viewModelScope

com.example.inventory.data.Item をインポートします(自動的にインポートされない場合)。

  1. InventoryViewModel クラスで、3 つの文字列を受け取り Item インスタンスを返す別のプライベート関数を追加します。
private fun getNewItemEntry(itemName: String, itemPrice: String, itemCount: String): Item {
   return Item(
       itemName = itemName,
       itemPrice = itemPrice.toDouble(),
       quantityInStock = itemCount.toInt()
   )
}
  1. さらに 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 を更新する

  1. AddItemFragment.kt で、AddItemFragment クラスの先頭に、InventoryViewModel 型の viewModel という private val を作成します。Kotlin プロパティのデリゲート by activityViewModels() を使用して、フラグメント間で ViewModel を共有します。エラーは次のステップで修正します。
private val viewModel: InventoryViewModel by activityViewModels {
}
  1. ラムダ内で、InventoryViewModelFactory() コンストラクタを呼び出し、ItemDao インスタンスを渡します。以前のタスクで作成した database インスタンスを使用して、itemDao コンストラクタを呼び出します。
private val viewModel: InventoryViewModel by activityViewModels {
   InventoryViewModelFactory(
       (activity?.application as InventoryApplication).database
           .itemDao()
   )
}
  1. viewModel 定義の下で、Item 型の item という lateinit var を作成します。
 lateinit var item: Item
  1. [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
}
  1. AddItemFragment.kt で、onCreateView() 関数の下に、Boolean を返す isEntryValid() という private 関数を作成します。次のステップで戻り値欠落エラーを修正します。
private fun isEntryValid(): Boolean {
}
  1. 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()
   )
}
  1. isEntryValid() 関数の下の AddItemFragment クラスで、パラメータなしで addNewItem() という別の private 関数を追加し、何も返しません。関数内で、if 条件内で isEntryValid() を呼び出します。
private fun addNewItem() {
   if (isEntryValid()) {
   }
}
  1. if ブロック内で、viewModel インスタンスに対して addNewItem() メソッドを呼び出します。ユーザーが入力したアイテムの詳細を渡し、binding インスタンスを使用して読み取ります。
if (isEntryValid()) {
   viewModel.addNewItem(
   binding.itemName.text.toString(),
   binding.itemPrice.text.toString(),
   binding.itemCount.text.toString(),
   )
}
  1. if ブロックの下で、val action を作成して ItemListFragment に戻ります。findNavController().navigate() を呼び出して action を渡します。
val action = AddItemFragmentDirections.actionAddItemFragmentToItemListFragment()
findNavController().navigate(action)

androidx.navigation.fragment.findNavController. をインポートします。

  1. 完成したメソッドは次のようになります。
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)
       }
}
  1. すべてをまとめるために、[SAVE] ボタンにクリック ハンドラを追加します。AddItemFragment クラスの、onDestroyView() 関数の上で、onViewCreated() 関数をオーバーライドします。
  2. onViewCreated() 関数内で、保存ボタンにクリック ハンドラを追加し、そこから addNewItem() を呼び出します。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   super.onViewCreated(view, savedInstanceState)
   binding.saveAction.setOnClickListener {
       addNewItem()
   }
}
  1. アプリをビルドして実行します。[+] FAB をタップします。[Add Item] 画面でアイテムの詳細を追加して [SAVE] をタップします。この操作を行うとデータは保存されますが、アプリにはまだ何も表示されません。次のタスクでは Database Inspector を使用して、保存したデータを表示します。

193c7fa9c41e0819.png

Database Inspector を使用してデータベースを表示する

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

4803c08f94e34118.png

お疲れさまでした。Room を使用してデータを永続化するアプリを作成しました。次の Codelab では、アプリに RecyclerView を追加してデータベース上のアイテムを表示し、エンティティの削除や更新などの新機能をアプリに追加します。ご参加をお待ちしております。

10. 解答コード

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

この Codelab のコードを取得して Android Studio で開く手順は次のとおりです。

コードを取得する

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

5b0a76c50478a73f.png

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

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

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

36cc44fcf0f89a1d.png

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

21f3eec988dcfbe9.png

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

11. 概要

  • テーブルを、@Entity アノテーション付きのデータクラスとして定義する。@ColumnInfo アノテーション付きのプロパティを、テーブルの列として定義する。
  • データ アクセス オブジェクト(DAO)を、@Dao アノテーション付きのインターフェースとして定義する。DAO は、Kotlin 関数をデータベース クエリにマッピングする。
  • アノテーションを使用して、@Insert@Delete@Update 関数を定義する。
  • SQLite クエリ文字列の @Query アノテーションを、他のクエリのパラメータとして使用する。
  • Database Inspector を使用して、Android SQLite データベースに保存されているデータを表示する。

12. 詳細

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

ブログ投稿

動画

その他のドキュメントと記事