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 デベロッパー ドキュメント
ブログ投稿
動画
その他のドキュメントと記事