1. 始める前に
ほとんどの製品版品質のアプリには、保持する必要のあるデータがあります。たとえば、曲のプレイリスト、To-Do リストの項目、収支の記録、星座表、個人データの履歴などが挙げられます。そのようなユースケースでは、この永続データの保存にデータベースを使用します。
Room は、Android Jetpack の一部である永続ライブラリで、SQLite データベースの上に位置する抽象化レイヤです。SQLite は専門言語(SQL)を使用してデータベース操作を行います。SQLite を直接使用する代わりに Room を使用すると、データベースのセットアップ、構成、アプリの操作が簡単になります。Room には、SQLite ステートメントのコンパイル時チェック機能もあります。
抽象化レイヤは、基となる実装や複雑さを隠す機能のセットです。この場合、SQLite のような既存の機能セットに対するインターフェースを提供します。
下図に、このコースで推奨されているアーキテクチャ全体におけるデータソースとしての Room の位置付けを示します。Room はデータソースです。
前提条件
- Jetpack Compose を使用して Android アプリの基本的なユーザー インターフェース(UI)を作成できること。
Text
、Icon
、IconButton
、LazyColumn
などのコンポーザブルを使用できること。NavHost
コンポーザブルを使用して、アプリのルートと画面を定義できること。NavHostController
を使用して画面間を移動できること。- Android アーキテクチャ コンポーネント
ViewModel
に精通していること。ViewModelProvider.Factory
を使用して ViewModel をインスタンス化できること。 - 同時実行の基本に精通していること。
- 長時間実行タスクにコルーチンを使用できること。
- SQLite データベースと SQL 言語に関する基本的な知識があること。
学習内容
- Room ライブラリを使用して SQLite データベースを作成し、操作する方法。
- エンティティ、データ アクセス オブジェクト(DAO)、データベース クラスを作成する方法。
- DAO を使用して Kotlin 関数を SQL クエリにマッピングする方法。
作成するアプリの概要
- インベントリ アイテムを SQLite データベースに保存する Inventory アプリを作成します。
必要なもの
- Inventory アプリのスターター コード
- Android Studio がインストールされているパソコン
- API レベル 26 以降を搭載しているデバイスまたはエミュレータ
2. アプリの概要
この Codelab では、Inventory アプリのスターター コードを扱い、Room ライブラリを使用してアプリにデータベース レイヤを追加します。アプリの最終バージョンでは、在庫データベースからアイテムのリストを表示します。ユーザーは、新しいアイテムの追加、既存のアイテムの更新、在庫データベースからのアイテムの削除を行えます。この Codelab では、アイテムデータを Room データベースに保存します。アプリの残りの機能については、次の Codelab で説明します。
3. スターター アプリの概要
この Codelab のスターター コードをダウンロードする
まず、スターター コードをダウンロードします。
または、GitHub リポジトリのクローンを作成してコードを入手することもできます。
$ 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 starter
コードは Inventory app
GitHub リポジトリで確認できます。
スターター コードの概要
- Android Studio でスターター コードのプロジェクトを開きます。
- Android デバイスまたはエミュレータでアプリを実行します。エミュレータまたは接続済みのデバイスが API レベル 26 以降で動作していることを確認します。Database Inspector は、API レベル 26 以降を搭載したエミュレータやデバイスで機能します。
- アプリに在庫データが表示されていないことを確認します。
- フローティング アクション ボタン(FAB)をタップすると、データベースに新しいアイテムを追加できます。
自動で新しい画面に移動し、新しいアイテムの詳細情報を入力できるようになります。
スターター コードの問題点
- [Add Item] 画面で、アイテムの詳細(名前、価格、数量など)を入力します。
- [Save] をタップします。[Add Item] 画面は閉じませんが、戻るキーを使用して戻ることができます。保存機能が実装されていないため、アイテムの詳細は保存されません。
アプリは未完成であり、[Save] ボタンの機能が実装されていません。
この Codelab では、Room を使用してインベントリの詳細を SQLite データベースに保存するコードを追加します。Room 永続ライブラリを使用して SQLite データベースを操作します。
コードのチュートリアル
ダウンロードしたスターター コードには、画面のレイアウトがあらかじめ用意されています。ここでは、データベース ロジックの実装に焦点を当てます。作業の土台とするファイルの一部について簡単に説明します。
ui/home/HomeScreen.kt
このファイルはホーム画面、つまりアプリの最初の画面であり、在庫リストを表示するコンポーザブルが含まれています。新しいアイテムをリストに追加する FAB があります。後ほどこのアイテムをリスト表示します。
ui/item/ItemEntryScreen.kt
この画面は ItemEditScreen.kt
に似ています。どちらにもアイテムの詳細のテキスト フィールドがあります。この画面は、ホーム画面で FAB をタップすると表示されます。ItemEntryViewModel.kt
は、この画面に対応する ViewModel
です。
ui/navigation/InventoryNavGraph.kt
このファイルは、アプリ全体のナビゲーション グラフです。
4. Room の主なコンポーネント
Kotlin では、データクラスを通じてデータを簡単に扱えます。データクラスを使用してインメモリ データを扱うのは簡単ですが、データを永続化するとなると、そのデータをデータベース ストレージと互換性のある形式に変換する必要があります。そのためには、データを格納するためのテーブルと、データにアクセスして変更するためのクエリが必要です。
以下に示す Room の 3 つのコンポーネントを使用すると、こうしたワークフローがシームレスになります。
- Room エンティティは、アプリのデータベースのテーブルを表します。テーブルの行に格納されているデータの更新や、挿入するための新しい行の作成に使用します。
- Room DAO は、データベース内のデータを取得、更新、挿入、削除するためにアプリで使用するメソッドを提供します。
- Room Database クラスは、データベースに関連付けられている DAO のインスタンスをアプリに提供するデータベース クラスです。
これらのコンポーネントの実装と詳細については、この Codelab で後ほど説明します。下図に、Room のコンポーネントが連携してデータベースを操作する仕組みを示します。
Room の依存関係を追加する
このタスクでは、必要な Room コンポーネント ライブラリを Gradle ファイルに追加します。
- モジュール レベルの Gradle ファイル
build.gradle.kts (Module: InventoryApp.app)
を開きます。 dependencies
ブロックに、次のコードに示す Room ライブラリの依存関係を追加します。
//Room
implementation("androidx.room:room-runtime:${rootProject.extra["room_version"]}")
ksp("androidx.room:room-compiler:${rootProject.extra["room_version"]}")
implementation("androidx.room:room-ktx:${rootProject.extra["room_version"]}")
KSP は、Kotlin アノテーションを解析するための強力かつシンプルな API です。
5. アイテム エンティティを作成する
Entity クラスはテーブルを定義します。このクラスの各インスタンスは、データベース テーブルの行を表します。エンティティ クラスには、データベース内の情報の表示方法や操作方法を Room に伝えるためのマッピングがあります。今回のアプリでは、エンティティはアイテム名、アイテム価格、アイテムの残り数量など、在庫アイテムに関する情報を保持します。
@Entity
アノテーションは、クラスをデータベースの Entity クラスとしてマークします。アプリは Entity クラスごとに、アイテムを保持するデータベース テーブルを作成します。Entity の各フィールドは、特に明記されていない限り、データベースの列として表されます(詳細については Entity のドキュメントをご覧ください)。データベースに格納されるすべてのエンティティ インスタンスに主キーが必要です。主キーは、データベース テーブルのすべてのレコードやエントリを一意に識別するために使用します。アプリが割り当てた後に主キーを変更することはできません。データベースに存在する限り、エンティティ オブジェクトを表します。
このタスクでは、Entity クラスを作成し、アイテムごとに在庫情報を格納するフィールドを定義します(主キーの格納は Int
、アイテム名の格納は String
、アイテム価格の格納は double
、在庫数の格納は Int
)。
- Android Studio でスターター コードを開きます。
com.example.inventory
基本パッケージの下にあるdata
パッケージを開きます。data
パッケージ内で、アプリのデータベース エンティティを表すItem
Kotlin クラスを開きます。
// No need to copy over, this is part of the starter code
class Item(
val id: Int,
val name: String,
val price: Double,
val quantity: Int
)
データクラス
データクラスは、主に Kotlin でデータを保持するために使用します。data
キーワードで定義します。Kotlin データクラス オブジェクトには、いくつかの利点があります。たとえば、コンパイラが比較、出力、コピーのためのユーティリティ(toString()
、copy()
、equals()
など)を自動的に生成します。
例:
// Example data class with 2 properties.
data class User(val firstName: String, val lastName: String){
}
生成されるコードに一貫性を持たせ、有意義な動作をさせるために、データクラスは次の要件を満たす必要があります。
- プライマリ コンストラクタには少なくとも 1 つのパラメータが必要です。
- プライマリ コンストラクタのパラメータはすべて、
val
またはvar
にする必要があります。 - データクラスを
abstract
、open
、sealed
にすることはできません。
データクラスの詳細については、データクラスのドキュメントをご覧ください。
Item
クラスの定義の前にdata
キーワードを付けて、データクラスに変換します。
data class Item(
val id: Int,
val name: String,
val price: Double,
val quantity: Int
)
Item
クラス宣言の上で、データクラスに@Entity
アノテーションを付けます。tableName
引数を使用して、items
を SQLite テーブル名として設定します。
import androidx.room.Entity
@Entity(tableName = "items")
data class Item(
...
)
id
プロパティに@PrimaryKey
アノテーションを付けて、id
を主キーにします。主キーは、Item
テーブルのすべてのレコードやエントリを一意に識別する ID です。
import androidx.room.PrimaryKey
@Entity(tableName = "items")
data class Item(
@PrimaryKey
val id: Int,
...
)
id
にデフォルト値として0
を割り当てます。これは、id
がid
値を自動生成するために必要です。autoGenerate
パラメータを@PrimaryKey
アノテーションに追加して、主キー列を自動生成するかどうかを指定します。autoGenerate
がtrue
に設定されている場合、新しいエンティティ インスタンスがデータベースに挿入されると、Room は主キー列の一意の値を自動生成します。これにより、各エンティティ インスタンスに一意の識別子が割り当てられ、主キー列に手動で値を割り当てる必要がなくなります。
data class Item(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
// ...
)
これで、Entity クラスが作成されたので、データベースにアクセスするためのデータアクセス オブジェクト(DAO)を作成できます。
6. アイテム DAO を作成する
データ アクセス オブジェクト(DAO)は、抽象インターフェースを提供することで永続化レイヤをアプリの残りの部分から分離するために使用できるパターンです。この分離は、これまでの Codelab で見てきた単一責任の原則に則したものです。
DAO の機能は、基となる永続化レイヤでのデータベース操作に関連するすべての複雑さを隠し、アプリの残りの部分から分離することです。これにより、データを使用するコードから独立してデータレイヤを変更できます。
このタスクでは、Room の DAO を定義します。DAO は、データベースにアクセスするインターフェースを定義する Room の主要コンポーネントです。
作成する DAO はカスタム インターフェースです。データベースのクエリ、取得、挿入、削除、更新を簡単に行うことができます。Room はコンパイル時にこのクラスの実装を生成します。
Room
ライブラリには、@Insert
、@Delete
、@Update
などの便利なアノテーションが用意されており、SQL ステートメントを記述せずに簡単な挿入、削除、更新を行うメソッドを定義できます。
より複雑な挿入、削除、更新のオペレーションを定義する必要がある場合や、データベース内のデータにクエリを行う必要がある場合は、代わりに @Query
アノテーションを使用します。
さらに、Android Studio でクエリを記述すると、コンパイラが SQL クエリの構文エラーをチェックします。
Inventory アプリの場合、次のことを行える必要があります。
- 新しいアイテムを挿入または追加する。
- 既存のアイテムを更新して、名前、価格、数量を更新する。
- 主キーである
id
に基づいて、特定のアイテムを取得する。 - すべてのアイテムを取得して、表示できるようにする。
- データベースのエントリを削除する。
アプリにアイテム DAO を実装する手順は次のとおりです。
data
パッケージで、Kotlin インターフェースItemDao.kt
を作成します。
ItemDao
インターフェースに@Dao
アノテーションを付けます。
import androidx.room.Dao
@Dao
interface ItemDao {
}
- インターフェースの本体内に
@Insert
アノテーションを追加します。 @Insert
の下に、Entity
クラスのitem
のインスタンスを引数として取るinsert()
関数を追加します。- 関数を
suspend
キーワードでマークすると、別のスレッドで実行できます。
データベース操作は実行に時間がかかる可能性があるため、別のスレッドで実行する必要があります。Room ではメインスレッドでのデータベース アクセスができません。
import androidx.room.Insert
@Insert
suspend fun insert(item: Item)
データベースにアイテムを挿入する際、競合が発生することがあります。たとえば、コード内の複数の箇所で、異なる競合する値(同じ主キーなど)でエンティティを更新しようとする場合です。エンティティはデータベース内の行です。Inventory アプリでは、[Add Item] 画面からでないとエンティティを挿入できないため、競合は想定されていません。競合戦略は [Ignore] に設定できます。
- 引数
onConflict
を追加し、値OnConflictStrategy.
IGNORE
を割り当てます。
引数 onConflict
は、競合が発生した場合の処理を Room に伝えます。OnConflictStrategy.
IGNORE
戦略は、新しいアイテムを無視します。
利用可能な競合戦略について詳しくは、OnConflictStrategy
のドキュメントをご覧ください。
import androidx.room.OnConflictStrategy
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(item: Item)
これで、item
をデータベースに挿入するために必要なすべてのコードが Room
によって生成されるようになりました。Room アノテーションが付いている DAO 関数を呼び出すと、Room はデータベースに対して対応する SQL クエリを実行します。たとえば、上記のメソッド insert()
を Kotlin コードから呼び出すと、Room
は SQL クエリを実行してエンティティをデータベースに挿入します。
Item
をパラメータとして受け取る、@Update
アノテーションを付けた新しい関数を追加します。
渡されるエンティティと同じ主キーを持つエンティティが更新されます。エンティティの他のプロパティの一部または全部を更新できます。
insert()
メソッドと同様に、この関数にsuspend
キーワードを付けます。
import androidx.room.Update
@Update
suspend fun update(item: Item)
@Delete
アノテーションを付けた別の関数を追加してアイテムを削除し、suspend 関数にします。
import androidx.room.Delete
@Delete
suspend fun delete(item: Item)
残りの機能には便利なアノテーションがないため、@Query
アノテーションを使用して SQLite クエリを指定する必要があります。
- 指定した
id
に基づいてアイテム テーブルから特定のアイテムを取得する SQLite クエリを記述します。次のコードは、items
から、id
が特定の値に一致する列をすべて選択するサンプルクエリを示しています。id
は一意の識別子です。
例:
// Example, no need to copy over
SELECT * from items WHERE id = 1
@Query
アノテーションを追加します。- 前のステップの SQLite クエリを、
@Query
アノテーションの文字列パラメータとして使用します。 String
パラメータを@Query
に追加します。これはアイテム テーブルからアイテムを取得する SQLite クエリです。
このクエリでは、id
が :id
引数と一致する items
からすべての列が選択されることになります。:id
はクエリ内でコロン表記を使用して、関数内の引数を参照しています。
@Query("SELECT * from items WHERE id = :id")
@Query
アノテーションの後に、Int
引数を受け取ってFlow<Item>
を返すgetItem()
関数を追加します。
import androidx.room.Query
import kotlinx.coroutines.flow.Flow
@Query("SELECT * from items WHERE id = :id")
fun getItem(id: Int): Flow<Item>
永続化レイヤでは Flow
を使用することをおすすめします。戻り値の型として Flow
を指定すると、データベース内のデータが変更されるたびに通知が届きます。Room
がこの Flow
を最新の状態に維持します。つまり、データを明示的に取得する必要があるのは一度だけです。このセットアップは次の Codelab で実装する在庫リストを更新する際に役立ちます。戻り値の型が Flow
であるため、Room はバックグラウンド スレッドでクエリを実行します。明示的に suspend
関数にしてコルーチン スコープ内で呼び出す必要はありません。
@Query
とgetAllItems()
関数を追加します。- SQLite クエリが
item
テーブルのすべての列を昇順で返すようにします。 getAllItems()
がItem
エンティティのリストをFlow
として返すようにします。Room
がこのFlow
を最新の状態に維持します。つまり、データを明示的に取得する必要があるのは一度だけです。
@Query("SELECT * from items ORDER BY name ASC")
fun getAllItems(): Flow<List<Item>>
完成した ItemDao
:
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import kotlinx.coroutines.flow.Flow
@Dao
interface ItemDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(item: Item)
@Update
suspend fun update(item: Item)
@Delete
suspend fun delete(item: Item)
@Query("SELECT * from items WHERE id = :id")
fun getItem(id: Int): Flow<Item>
@Query("SELECT * from items ORDER BY name ASC")
fun getAllItems(): Flow<List<Item>>
}
- 目に見える変化はありませんが、アプリをビルドしてエラーがないことを確認します。
7. データベース インスタンスを作成する
このタスクでは、これまでのタスクで扱った Entity
と DAO を使用する RoomDatabase
を作成します。データベース クラスは、エンティティのリストと DAO を定義します。
Database
クラスは、定義した DAO のインスタンスをアプリに提供します。アプリはこの DAO を使用して、関連するデータ エンティティ オブジェクトのインスタンスとしてデータベースからデータを取得できます。また、定義されたデータ エンティティを使用して、対応するテーブルの行を更新したり、挿入用の新しい行を作成したりできます。
抽象 RoomDatabase
クラスを作成し、@Database
アノテーションを付ける必要があります。このクラスには、データベースが存在しない場合に RoomDatabase
の既存のインスタンスを返すメソッドが 1 つあります。
RoomDatabase
インスタンスを取得する一般的なプロセスは次のとおりです。
RoomDatabase
を拡張するpublic abstract
クラスを作成します。定義した新しい抽象クラスは、データベース ホルダーとして機能します。Room
が実装を作成するため、定義したクラスは抽象クラスです。- クラスに
@Database
アノテーションを付けます。引数で、データベースのエンティティをリストしてバージョン番号を設定します。 ItemDao
インスタンスを返す抽象メソッドまたはプロパティを定義すると、Room
が実装を生成します。- アプリ全体で必要な
RoomDatabase
のインスタンスは 1 つのみであるため、RoomDatabase
をシングルトンにします。 Room
のRoom.databaseBuilder
を使用して、存在しない場合にのみ(item_database
)データベースを作成します。それ以外の場合は、既存のデータベースを返します。
データベースを作成する
data
パッケージで、Kotlin クラスInventoryDatabase.kt
を作成します。InventoryDatabase.kt
ファイルで、InventoryDatabase
クラスを、RoomDatabase
を拡張するabstract
クラスにします。- クラスに
@Database
アノテーションを付けます。パラメータ欠落エラーは、次のステップで修正するため無視してください。
import androidx.room.Database
import androidx.room.RoomDatabase
@Database
abstract class InventoryDatabase : RoomDatabase() {}
@Database
アノテーションには、Room
がデータベースを構築できるように、複数の引数が必要です。
entities
のリストを持つ唯一のクラスとしてItem
を指定します。version
を1
に設定します。データベース テーブルのスキーマを変更するたびに、バージョン番号を増やす必要があります。- スキーマのバージョン履歴のバックアップを保持しないように、
exportSchema
をfalse
に設定します。
@Database(entities = [Item::class], version = 1, exportSchema = false)
- クラスの本体内で、
ItemDao
を返す抽象関数を宣言し、データベースが DAO を認識できるようにします。
abstract fun itemDao(): ItemDao
- 抽象関数の下で、
companion object
を定義します。これにより、データベースを作成または取得するメソッドにアクセスできるようにし、クラス名を修飾子として使用します。
companion object {}
companion
オブジェクト内で、データベース用に null 許容のプライベート変数Instance
を宣言し、null
に初期化します。
Instance
変数は、データベースの作成時に、データベースに対する参照を保持します。これは、ある時点で開かれているデータベースのインスタンス(作成と維持にコストのかかるリソース)を 1 つだけ維持する際に役立ちます。
Instance
に@Volatile
アノテーションを付けます。
volatile 変数の値はキャッシュに保存されません。読み取りと書き込みはすべてメインメモリとの間で行われます。こうした機能により、Instance
の値が常に最新になり、すべての実行スレッドで同じになります。つまり、あるスレッドが Instance
に加えた変更が、すぐに他のすべてのスレッドに反映されます。
@Volatile
private var Instance: InventoryDatabase? = null
Instance
の下、companion
オブジェクト内で、データベース ビルダーに必要なContext
パラメータを持つgetDatabase()
メソッドを定義します。InventoryDatabase
型を返します。getDatabase()
はまだ何も返していないため、エラー メッセージが表示されます。
import android.content.Context
fun getDatabase(context: Context): InventoryDatabase {}
複数のスレッドがデータベース インスタンスを同時に要求し、結果的に 1 つではなく 2 つのデータベースが作成される可能性があります。この現象を競合状態と呼びます。データベースを取得するコードを synchronized
ブロックで囲むと、このコードブロックには一度に 1 つの実行スレッドしか入ることができず、データベースは一度だけ初期化されます。競合状態を回避するため、synchronized{}
ブロックを使用します。
getDatabase()
内でInstance
変数を返します。または、Instance
が null の場合はsynchronized{}
ブロック内で初期化します。これにはエルビス演算子(?:
)を使用します。- コンパニオン オブジェクトの
this
を渡します。エラーは後ほど修正します。
return Instance ?: synchronized(this) { }
- synchronized ブロック内で、データベース ビルダーを使用してデータベースを取得します。エラーは次のステップで修正するため無視してください。
import androidx.room.Room
Room.databaseBuilder()
synchronized
ブロック内で、データベース ビルダーを使用してデータベースを取得します。アプリ コンテキスト、データベース クラス、データベースの名前item_database
をRoom.databaseBuilder()
に渡します。
Room.databaseBuilder(context, InventoryDatabase::class.java, "item_database")
Android Studio が型の不一致エラーを生成します。このエラーを解消するには、以降のステップで build()
を追加する必要があります。
- 必要な移行戦略をビルダーに追加します。
.
fallbackToDestructiveMigration()
を使用します。
.fallbackToDestructiveMigration()
- データベース インスタンスを作成するために、
.build()
を呼び出します。この呼び出しにより、Android Studio のエラーが解消されます。
.build()
build()
の後に、also
ブロックを追加し、Instance = it
を割り当てて、最近作成された db インスタンスへの参照を保持します。
.also { Instance = it }
synchronized
ブロックの最後でinstance
を返します。コードは最終的に次のようになります。
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
/**
* Database class with a singleton Instance object.
*/
@Database(entities = [Item::class], version = 1, exportSchema = false)
abstract class InventoryDatabase : RoomDatabase() {
abstract fun itemDao(): ItemDao
companion object {
@Volatile
private var Instance: InventoryDatabase? = null
fun getDatabase(context: Context): InventoryDatabase {
// if the Instance is not null, return it, otherwise create a new database instance.
return Instance ?: synchronized(this) {
Room.databaseBuilder(context, InventoryDatabase::class.java, "item_database")
.build()
.also { Instance = it }
}
}
}
}
- コードをビルドして、エラーがないことを確認します。
8. リポジトリを実装する
このタスクでは、ItemsRepository
インターフェースと OfflineItemsRepository
クラスを実装して、データベースの get
、insert
、delete
、update
エンティティを提供します。
data
パッケージのItemsRepository.kt
ファイルを開きます。- DAO 実装にマッピングする次の関数をインターフェースに追加します。
import kotlinx.coroutines.flow.Flow
/**
* Repository that provides insert, update, delete, and retrieve of [Item] from a given data source.
*/
interface ItemsRepository {
/**
* Retrieve all the items from the the given data source.
*/
fun getAllItemsStream(): Flow<List<Item>>
/**
* Retrieve an item from the given data source that matches with the [id].
*/
fun getItemStream(id: Int): Flow<Item?>
/**
* Insert item in the data source
*/
suspend fun insertItem(item: Item)
/**
* Delete item from the data source
*/
suspend fun deleteItem(item: Item)
/**
* Update item in the data source
*/
suspend fun updateItem(item: Item)
}
data
パッケージのOfflineItemsRepository.kt
ファイルを開きます。ItemDao
型のコンストラクタ パラメータを渡します。
class OfflineItemsRepository(private val itemDao: ItemDao) : ItemsRepository
OfflineItemsRepository
クラスで、ItemsRepository
インターフェースで定義された関数をオーバーライドし、ItemDao
から対応する関数を呼び出します。
import kotlinx.coroutines.flow.Flow
class OfflineItemsRepository(private val itemDao: ItemDao) : ItemsRepository {
override fun getAllItemsStream(): Flow<List<Item>> = itemDao.getAllItems()
override fun getItemStream(id: Int): Flow<Item?> = itemDao.getItem(id)
override suspend fun insertItem(item: Item) = itemDao.insert(item)
override suspend fun deleteItem(item: Item) = itemDao.delete(item)
override suspend fun updateItem(item: Item) = itemDao.update(item)
}
AppContainer クラスを実装する
このタスクでは、データベースをインスタンス化し、DAO インスタンスを OfflineItemsRepository
クラスに渡します。
data
パッケージのAppContainer.kt
ファイルを開きます。ItemDao()
インスタンスをOfflineItemsRepository
コンストラクタに渡します。- データベース インスタンスをインスタンス化するには、
InventoryDatabase
クラスでgetDatabase()
を呼び出してコンテキストを渡し、.itemDao()
を呼び出してDao
のインスタンスを作成します。
override val itemsRepository: ItemsRepository by lazy {
OfflineItemsRepository(InventoryDatabase.getDatabase(context).itemDao())
}
これで、Room を扱うためのビルディング ブロックが揃いました。このコードはコンパイルされて実行されますが、実際に機能するかどうかを確認する方法はありません。そのため、ここでデータベースをテストすることをおすすめします。テストを実施するには、ViewModel
がデータベースとやりとりする必要があります。
9. 保存機能を追加する
これまでに、データベースを作成し、UI クラスはスターター コードに含まれていました。アプリの一時的なデータを保存し、データベースにアクセスするには、ViewModel
を更新する必要があります。ViewModel
は DAO を介してデータベースを操作し、UI にデータを提供します。データベース操作はすべてメイン UI スレッドから切り離す必要があるため、コルーチンと viewModelScope
を使用します。
UI 状態クラスのチュートリアル
ui/item/ItemEntryViewModel.kt
ファイルを開きます。ItemUiState
データクラスは、アイテムの UI 状態を表します。ItemDetails
データクラスは 1 つのアイテムを表します。
スターター コードには拡張関数が 3 つ用意されています。
ItemDetails.toItem()
拡張関数は、ItemUiState
UI 状態オブジェクトをItem
エンティティ型に変換します。Item.toItemUiState()
拡張関数は、Item
Room エンティティ オブジェクトをItemUiState
UI 状態型に変換します。Item.toItemDetails()
拡張関数は、Item
Room エンティティ オブジェクトをItemDetails
に変換します。
// No need to copy, this is part of starter code
/**
* Represents Ui State for an Item.
*/
data class ItemUiState(
val itemDetails: ItemDetails = ItemDetails(),
val isEntryValid: Boolean = false
)
data class ItemDetails(
val id: Int = 0,
val name: String = "",
val price: String = "",
val quantity: String = "",
)
/**
* Extension function to convert [ItemDetails] to [Item]. If the value of [ItemDetails.price] is
* not a valid [Double], then the price will be set to 0.0. Similarly if the value of
* [ItemDetails.quantity] is not a valid [Int], then the quantity will be set to 0
*/
fun ItemDetails.toItem(): Item = Item(
id = id,
name = name,
price = price.toDoubleOrNull() ?: 0.0,
quantity = quantity.toIntOrNull() ?: 0
)
fun Item.formatedPrice(): String {
return NumberFormat.getCurrencyInstance().format(price)
}
/**
* Extension function to convert [Item] to [ItemUiState]
*/
fun Item.toItemUiState(isEntryValid: Boolean = false): ItemUiState = ItemUiState(
itemDetails = this.toItemDetails(),
isEntryValid = isEntryValid
)
/**
* Extension function to convert [Item] to [ItemDetails]
*/
fun Item.toItemDetails(): ItemDetails = ItemDetails(
id = id,
name = name,
price = price.toString(),
quantity = quantity.toString()
)
ビューモデルで上記のクラスを使用して、UI の読み取りと更新を行います。
ItemEntry ViewModel を更新する
このタスクでは、リポジトリを ItemEntryViewModel.kt
ファイルに渡します。また、[Add Item] 画面で入力したアイテムの詳細をデータベースに保存します。
ItemEntryViewModel
クラスのvalidateInput()
プライベート関数に注目してください。
// No need to copy over, this is part of starter code
private fun validateInput(uiState: ItemDetails = itemUiState.itemDetails): Boolean {
return with(uiState) {
name.isNotBlank() && price.isNotBlank() && quantity.isNotBlank()
}
}
上記の関数は、name
、price
、quantity
が空かどうかを確認します。この関数を使用してユーザー入力を検証してから、データベースのエンティティを追加または更新します。
ItemEntryViewModel
クラスを開き、ItemsRepository
型のprivate
デフォルト コンストラクタ パラメータを追加します。
import com.example.inventory.data.ItemsRepository
class ItemEntryViewModel(private val itemsRepository: ItemsRepository) : ViewModel() {
}
ui/AppViewModelProvider.kt
のアイテム エントリ ビューモデルのinitializer
を更新し、パラメータとしてリポジトリ インスタンスを渡します。
object AppViewModelProvider {
val Factory = viewModelFactory {
// Other Initializers
// Initializer for ItemEntryViewModel
initializer {
ItemEntryViewModel(inventoryApplication().container.itemsRepository)
}
//...
}
}
ItemEntryViewModel.kt
ファイルに移動し、ItemEntryViewModel
クラスの最後にsaveItem()
という suspend 関数を追加して、Room データベースにアイテムを挿入します。この関数は、データをブロック以外の方法でデータベースに追加します。
suspend fun saveItem() {
}
- 関数内で
itemUiState
が有効であるかどうかを確認し、Item
型に変換して、Room がデータを認識できるようにします。 itemsRepository
に対してinsertItem()
を呼び出し、データを渡します。UI はこの関数を呼び出して、アイテムの詳細をデータベースに追加します。
suspend fun saveItem() {
if (validateInput()) {
itemsRepository.insertItem(itemUiState.itemDetails.toItem())
}
}
エンティティをデータベースに追加するために必要な関数がすべて追加されました。次のタスクでは、上記の関数を使用するように UI を更新します。
ItemEntryBody() コンポーザブルのチュートリアル
ui/item/ItemEntryScreen.kt
ファイルでは、ItemEntryBody()
コンポーザブルがスターター コードの一部として部分的に実装されています。ItemEntryScreen()
関数呼び出しのItemEntryBody()
コンポーザブルに注目してください。
// No need to copy over, part of the starter code
ItemEntryBody(
itemUiState = viewModel.itemUiState,
onItemValueChange = viewModel::updateUiState,
onSaveClick = { },
modifier = Modifier
.padding(innerPadding)
.verticalScroll(rememberScrollState())
.fillMaxWidth()
)
- UI 状態と
updateUiState
ラムダが関数パラメータとして渡されています。関数の定義を調べて、UI 状態がどのように更新されるかを確認します。
// No need to copy over, part of the starter code
@Composable
fun ItemEntryBody(
itemUiState: ItemUiState,
onItemValueChange: (ItemUiState) -> Unit,
onSaveClick: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
// ...
) {
ItemInputForm(
itemDetails = itemUiState.itemDetails,
onValueChange = onItemValueChange,
modifier = Modifier.fillMaxWidth()
)
Button(
onClick = onSaveClick,
enabled = itemUiState.isEntryValid,
shape = MaterialTheme.shapes.small,
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(R.string.save_action))
}
}
}
このコンポーザブルに ItemInputForm
と [Save] ボタンが表示されています。ItemInputForm()
コンポーザブルにはテキスト フィールドが 3 つ表示されています。[Save] は、テキスト フィールドにテキストが入力されている場合にのみ有効になります。isEntryValid
値は、すべてのテキスト フィールドのテキストが有効である場合(空白ではない場合)に true になります。
- コンポーズ可能な関数
ItemInputForm()
の実装を見ると、onValueChange
関数パラメータがあります。itemDetails
の値を、ユーザーがテキスト フィールドに入力した値に置き換えます。[Save] ボタンが有効になるまでに、itemUiState.itemDetails
には保存する必要がある値が設定されます。
// No need to copy over, part of the starter code
@Composable
fun ItemEntryBody(
//...
) {
Column(
// ...
) {
ItemInputForm(
itemDetails = itemUiState.itemDetails,
//...
)
//...
}
}
// No need to copy over, part of the starter code
@Composable
fun ItemInputForm(
itemDetails: ItemDetails,
modifier: Modifier = Modifier,
onValueChange: (ItemUiState) -> Unit = {},
enabled: Boolean = true
) {
Column(modifier = modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(16.dp)) {
OutlinedTextField(
value = itemUiState.name,
onValueChange = { onValueChange(itemDetails.copy(name = it)) },
//...
)
OutlinedTextField(
value = itemUiState.price,
onValueChange = { onValueChange(itemDetails.copy(price = it)) },
//...
)
OutlinedTextField(
value = itemUiState.quantity,
onValueChange = { onValueChange(itemDetails.copy(quantity = it)) },
//...
)
}
}
Save ボタンにクリック リスナーを追加する
すべてをまとめるために、[Save] ボタンにクリック ハンドラを追加します。クリック ハンドラ内でコルーチンを開始し、saveItem()
を呼び出してデータを Room データベースに保存します。
ItemEntryScreen.kt
のコンポーズ可能な関数ItemEntryScreen
内で、コンポーズ可能な関数rememberCoroutineScope()
を使用して、coroutineScope
というval
を作成します。
import androidx.compose.runtime.rememberCoroutineScope
val coroutineScope = rememberCoroutineScope()
ItemEntryBody
()
関数呼び出しを更新し、onSaveClick
ラムダ内でコルーチンを開始します。
ItemEntryBody(
// ...
onSaveClick = {
coroutineScope.launch {
}
},
modifier = modifier.padding(innerPadding)
)
ItemEntryViewModel.kt
ファイルのsaveItem()
関数実装で、itemUiState
が有効で、itemUiState
をItem
型に変換し、itemsRepository.insertItem()
を使用してデータベースに挿入しているかどうかを確認します。
// No need to copy over, you have already implemented this as part of the Room implementation
suspend fun saveItem() {
if (validateInput()) {
itemsRepository.insertItem(itemUiState.itemDetails.toItem())
}
}
ItemEntryScreen.kt
のコンポーズ可能な関数ItemEntryScreen
にあるコルーチン内で、viewModel.saveItem()
を呼び出してアイテムをデータベースに保存します。
ItemEntryBody(
// ...
onSaveClick = {
coroutineScope.launch {
viewModel.saveItem()
}
},
//...
)
ItemEntryViewModel.kt
ファイルでは saveItem()
に viewModelScope.launch()
が使用されていませんが、これはリポジトリ メソッドを呼び出すとき ItemEntryBody
()
に必要です。suspend 関数は、コルーチンまたは別の suspend 関数からしか呼び出せません。関数 viewModel.saveItem()
は suspend 関数です。
- アプリをビルドして実行します。
- + FAB をタップします。
- [Add Item] 画面でアイテムの詳細を追加して [Save] をタップします。[Save] ボタンをタップしても [Add Item] 画面は閉じません。
onSaveClick
ラムダで、viewModel.saveItem()
の呼び出しの後にnavigateBack()
の呼び出しを追加して、前の画面に戻るようにします。ItemEntryBody()
関数は次のコードのようになります。
ItemEntryBody(
itemUiState = viewModel.itemUiState,
onItemValueChange = viewModel::updateUiState,
onSaveClick = {
coroutineScope.launch {
viewModel.saveItem()
navigateBack()
}
},
modifier = modifier.padding(innerPadding)
)
- アプリを再度実行し、同じ手順でデータを入力して保存します。今回は [Inventory] 画面に戻ります。
この操作を行うとデータは保存されますが、アプリで在庫データを確認することはできません。次のタスクでは Database Inspector を使用して、保存したデータを表示します。
10. Database Inspector を使用してデータベースの内容を表示する
Database Inspector を使用すると、アプリを動作させながらアプリのデータベースの検査、クエリ、変更を行えます。これは、データベースのデバッグで特に役立ちます。Database Inspector は、プレーン SQLite と、Room などの SQLite の上に構築されたライブラリで動作します。Database Inspector は、API レベル 26 を搭載したエミュレータやデバイスで最適に機能します。
- API レベル 26 以降を搭載した接続済みデバイスまたはエミュレータでアプリを実行します。
- Android Studio で、メニューバーから [View] > [Tool Windows] > [App Inspection] を選択します。
- [Database Inspector] タブを選択します。
- [Database Inspector] ペインのプルダウン メニューから
com.example.inventory
を選択します(選択されていない場合)。Inventory アプリの item_database が [Databases] ペインに表示されます。
- [Databases] ペインで item_database のノードを開き、[Item] を選択して調べます。[Databases] ペインが空の場合はエミュレータを使用し、[Add Item] 画面からデータベースにアイテムを追加します。
- Database Inspector の [Live updates] チェックボックスをオンにすると、エミュレータまたはデバイス上で実行中のアプリを操作したときに、表示されるデータが自動的に更新されます。
おめでとうございます!Room を使用してデータを保持するアプリを作成できました。次の Codelab では、アプリに lazyColumn
を追加してデータベースのアイテムを表示し、エンティティの削除や更新などの新機能をアプリに追加します。ご参加をお待ちしております。
11. 解答コードを取得する
この Codelab の解答コードは GitHub リポジトリにあります。この Codelab の完成したコードをダウンロードするには、次の git コマンドを使用します。
$ 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 で表示します。
12. まとめ
- テーブルを、
@Entity
アノテーション付きのデータクラスとして定義する。@ColumnInfo
アノテーション付きのプロパティを、テーブルの列として定義する。 - データ アクセス オブジェクト(DAO)を、
@Dao
アノテーション付きのインターフェースとして定義する。DAO は、Kotlin 関数をデータベース クエリにマッピングする。 - アノテーションを使用して、
@Insert
、@Delete
、@Update
関数を定義する。 - SQLite クエリ文字列の
@Query
アノテーションを、他のクエリのパラメータとして使用する。 - Database Inspector を使用して、Android SQLite データベースに保存されているデータを表示する。
13. 関連リンク
Android デベロッパー ドキュメント
ブログ投稿
動画
その他のドキュメントと記事