Androidアプリ開発 Compose x Room を使って Database を操作する

はじめに
以前に の記事で、SQLiteOpenHelperを使ったSQLiteデーターベースアクセスを行いました。SQLiteOpenHelperを使った方法は、シンプルゆえにテーブル毎にSQLクエリを自前で実装しなければならず、ボイラープレートコードが多く発生します。また、SQLの構文チェックもしづらいため、記述間違いによるバグが起こりやすく、開発速度が上がりませんでした。 そこで今回は、Androidアプリ開発で推奨だれるRoomを導入してみました。パッケージのインストールがやや複雑で、つまづいた部分などを含め忘備録として残しておきます。
後半ではRoomを使ったサンプルコードを紹介します。 Google Codelab Room を使用してデータを永続化する のCodelaboのサンプルを参考に、Roomを使うために最小限の構成で制作してみました。ただし、アーキテクチャはCodelaboと同様なので、かなり実践的に使えるものとなってます。
開発環境
項目 | バージョン |
---|---|
macOS | 13.2 |
Android Studio | Hedgehog 2023.1.1 Patch 2 |
新規プロジェクトの作成
ここから解説するサンプルコードは、Android Studio
2023でEmpty Activity
の新規プロジェクトを作成したところからです。KotlinおよびComposeの使用、ビルドスクリプトは.kts
を前提に解説します。
パッケージ&プラグインのインストール
この記事でインストールしたパッケージ:
パッケージ名 | バージョン | 備考 |
---|---|---|
androidx.room:room-runtime | 2.6.1 | Roomで必要 |
androidx.room:room-compiler | 2.6.1 | Roomで必要 |
androidx.room:room-ktx | 2.6.1 | Roomで必要 |
androidx.lifecycle:lifecycle-viewmodel-compose | 2.7.0 | ViewModelで必要 |
使用するプラグイン:
項目 | バージョン | 備考 |
---|---|---|
com.google.devtools.ksp | 1.9.0-1.0.13 | Roomで必要 |
org.jetbrains.kotlin.android | 1.9.0 | デフォルト |
Room関係のパッケージをインストール
モジュール レベルの Gradle ファイル
build.gradle.kts
を開き、dependencies
ブロックに以下を追加します:
("androidx.room:room-runtime:${rootProject.extra["room_version"]}") // 追加
implementation("androidx.room:room-compiler:${rootProject.extra["room_version"]}") // 追加
ksp("androidx.room:room-ktx:${rootProject.extra["room_version"]}") // 追加
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0") // ViewModelのために追加 implementation
プロジェクト レベルの Gradle ファイル
build.gradle.kts
を開いて以下を追加します:
{
buildscript .apply {
extraset("room_version", "2.6.1")
}
}
AndroidX のリリース ページから、Roomの最新の安定版リリース バージョン番号を確認できます。
KSPプラグインのインストール
KSP(Kotlin Symbol Processing)
とは、Kotlinコードを処理するためのツールで、アノテーションプロセッサの一種です。Roomを使う際にKSPが必要となります。少しつまずきやすいのでバージョンなどに気をつけながらインストールしてください。KSPをインストールするためにプplugins
に以下を追加します:
{
plugins ...
("com.google.devtools.ksp") version "1.9.0-1.0.13"
id}
今回使用するKotlinのバージョンが1.9.0
だったので、KSPもそれに合わせる必要がありました。KSPにバージョンに関しては
GitHubのkspリリース
でご確認いただけます。
Javaバージョンを統一する
さらに、KSPとプロジェクトのJavaバージョンを合わせる必要がありました。あったため、アプリレベルのbuild.gradle.kts
を次のように変更しました:
{
compileOptions // sourceCompatibility = JavaVersion.VERSION_1_8
// targetCompatibility = JavaVersion.VERSION_1_8
= JavaVersion.VERSION_17
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility }
{
kotlinOptions // jvmTarget = "1.8"
= "17"
jvmTarget }
以上で、AndroidプロジェクトでRoomを使えるようになりました。ここからは実際にRoomを使ったプログラミングの例を紹介していきます。 なお冒頭にも述べましたが、これから紹介するソースコードは Google Codelab Room を使用してデータを永続化する のCodelabを参考に、アレンジしたものとなります。ViewModelやComposableの基礎的な知識がある前提で読み進めてください。アーキテクチャはMVVMですが、ファクトリーメソッドや、コンテナー、DIなどの理解が要求されます。
【Roomの導入 Step1】エンティティを作成する
まずは、データベースのテーブルを表すエンティティを作成します。Users.kt
として、次のようなデータクラスを作成しました。@Entity
アノテーションをつけることで、SQLiteのテーブル名と結びつく形です。
package com.apppppp.testroom.data
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "users")
data class Users(
PrimaryKey(autoGenerate = true)
@val id: Int = 0,
val name: String,
val age: Int
)
Roomを使うと、このデータクラスを元に自動でテーブルを作ってくれます。つまり、下記のSQL文を発行する手間がなくなるわけです。とっても便利。
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
NOT NULL,
name TEXT INTEGER NOT NULL
age );
【Roomの導入 Step2】DAOを作成する
DAOはデータ アクセス オブジェクトと呼ばれるもので、データベースとアプリケーション(ロジック)の間を取り持つ、抽象インターフェースです。要するに、データベースのSQLクエリを抽象化して、データベース操作を簡単にしてくれるです。
package com.apppppp.testroom.data
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 UsersDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(item: Users)
suspend
@Update
suspend fun update(item: Users)
@Delete
suspend fun delete(item: Users)
@Query("SELECT * from users WHERE id = :id")
(id: Int): Flow<Users>
fun getItem
Query("SELECT * from users ORDER BY name ASC")
@fun getAllItems(): Flow<List<Users>>
}
本来はSQLを発行して行うロジック実装を、こんな簡潔に書くことができます。Room恐るべし。
【Roomの導入 Step3】データベース インスタンスを作成する
データベースのインスタンスを作成します。これは
SQLiteOpenHelper
のようなものだと理解しています。
SQLiteOpenHelper
と同様にシングルトンで発行し、アプリ内でインスタンスをひとつだけ持つようにします。このクラスは、のちに紹介するApplication
を継承したクラスで呼び出します
package com.apppppp.testroom.data
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 = [Users::class], version = 1, exportSchema = false)
abstract class MyDatabase : RoomDatabase() {
abstract fun usersDao(): UsersDao
companion object {
@Volatile
private var INSTANCE: MyDatabase? = null
fun getDatabase(context: Context): MyDatabase {
return INSTANCE ?: synchronized(this) {
.databaseBuilder(context, MyDatabase::class.java, "my_database").build().also { INSTANCE = it }
Room}
}
}
}
【Roomの導入 Step4】リポジトリを実装する
Roomのベースができたところで、ここからはリポジトリを作成します。リポジトリは、ViewModelなどへデータを提供したり処理したりするための、Daoとの仲介役となります。
リポジトリのインターフェースの作成
まずはインターフェースを作成します:
package com.apppppp.testroom.data
import kotlinx.coroutines.flow.Flow
interface UsersRepository {
fun getAllUsers(): Flow<List<Users>>
fun getUserStream(id: Int): Flow<Users>
suspend fun insertUser(user: Users)
suspend fun updateUser(user: Users)
suspend fun deleteUser(user: Users)
}
リポジトリインターフェースの実装
このインターフェースを実装したクラスを作成します:
package com.apppppp.testroom.data
import kotlinx.coroutines.flow.Flow
class OfflineUsersRepository(private val userDao: UsersDao) : UsersRepository {
override fun getAllUsers(): Flow<List<Users>> = userDao.getAllItems()
override fun getUserStream(id: Int): Flow<Users> = userDao.getItem(id)
override suspend fun insertUser(user: Users) = userDao.insert(user)
override suspend fun updateUser(user: Users) = userDao.update(user)
override suspend fun deleteUser(user: Users) = userDao.delete(user)
}
コンテナの作成
リポジトリはViewModelなどから直接生成するのではなく、次のようなコンテナを使ってインスタンス化させます。
package com.apppppp.testroom.data
import android.content.Context
interface AppContainer {
val usersRepository: UsersRepository
}
class AppDataContainer(private val context: Context) : AppContainer {
override val usersRepository: UsersRepository by lazy {
(MyDatabase.getDatabase(context).usersDao())
OfflineUsersRepository}
}
【Roomの導入 Step5】Applicationの実装
データベースのインスタンスを呼び出すために、Applicationを継承したカスタムクラスを作成します。ここではMyApplication
としました。
package com.apppppp.testroom
import android.app.Application
import com.apppppp.testroom.data.AppContainer
import com.apppppp.testroom.data.AppDataContainer
class MyApplication: Application() {
/**
* AppContainer instance used by the rest of classes to obtain dependencies
*/
lateinit var container: AppContainer
override fun onCreate() {
super.onCreate()
= AppDataContainer(this)
container }
}
クラスを作成したら、マニフェストのapplication
タグに追加します:
application
< android:name=".MyApplication"
...
これで、アプリ起動時にMyApplication
クラスが呼び出され、データベースインスタンスがシングルトンで一度だけ生成されます。以上でRoomの設定は終わりです。ここからは、ViewModelでリポジトリを介してデータのやり取りを行うサンプルを紹介します。
【Roomの導入 Step6】AppViewModelProvider
Codelabを参考に、複数のViewModelを管理できるProviderを作ってみました:
package com.apppppp.testroom.ui
import android.app.Application
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.apppppp.testroom.MyApplication
import com.apppppp.testroom.ui.home.HomeViewModel
object AppViewModelProvider {
val Factory = viewModelFactory {
// Initializer for HomeViewModel
{
initializer (inventoryApplication().container.usersRepository)
HomeViewModel}
}
}
/**
* Extension function to queries for [Application] object and returns an instance of
* [MyApplication].
*/
fun CreationExtras.inventoryApplication(): MyApplication =
(this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as MyApplication)
【Roomの導入 Step7】UIの実装
あとはいつも通り、Composeを使ったUIの実装を行なっていきます。今回はテストですので、一画面のみの実装となります。
HomeScreen
フロートボタンを設置して、ボタンを押すとデータベースにデータが挿入されます。そしてそのデータをLazyColumn
でリスト表示させます。リストアイテムをクリックすると、そのデータがデータベースから削除され、UIも更新される仕組みにっています。
package com.apppppp.testroom.ui.home
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.apppppp.testroom.ui.AppViewModelProvider
import androidx.lifecycle.viewmodel.compose.viewModel
import com.apppppp.testroom.data.Users
@Composable
fun HomeScreen(
modifier: Modifier = Modifier,
viewModel: HomeViewModel = viewModel(factory = AppViewModelProvider.Factory)
) {
val homeUiState by viewModel.homeUiState.collectAsState()
(
Scaffold= {
topBar (
Text= "ROOMテスト",
text = MaterialTheme.typography.titleLarge,
style = Modifier.padding(16.dp)
modifier )
},
= {
floatingActionButton (
FloatingActionButton= {viewModel.addUser()},
onClick = MaterialTheme.shapes.medium,
shape ) {
(
Icon= Icons.Default.Add,
imageVector = "追加"
contentDescription )
}
},
) { innerPadding ->
(content = {
LazyColumn(items = homeUiState.userList, key = { it.id }) { user ->
items(
HomeListItem= user,
user = Modifier
modifier .clickable {
.deleteUser(user)
viewModel}
)
}
},
= Modifier
modifier .padding(innerPadding)
)
}
}
@Composable
fun HomeListItem(user: Users, modifier: Modifier = Modifier) {
(
Text= "${user.id} : ${user.name} (${user.age})",
text = modifier.padding(16.dp)
modifier )
}
HomeViewModel
package com.apppppp.testroom.ui.home
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.apppppp.testroom.data.Users
import com.apppppp.testroom.data.UsersRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class HomeViewModel(private val usersRepository: UsersRepository): ViewModel() {
/**
* Holds home ui state. The list of items are retrieved from [UsersRepository] and mapped to
* [HomeUiState]
*/
val homeUiState: StateFlow<HomeUiState> =
.getAllUsers().map { HomeUiState(it) }
usersRepository.stateIn(
= viewModelScope,
scope = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS),
started = HomeUiState()
initialValue )
companion object {
private const val TIMEOUT_MILLIS = 5_000L
}
fun addUser() {
.launch {
viewModelScopeval newUser = Users(name = "New User", age = 30)
.insertUser(newUser)
usersRepository}
}
fun deleteUser(user: Users) {
.launch {
viewModelScope.deleteUser(user)
usersRepository}
}
}
/**
* Ui State for HomeScreen
*/
data class HomeUiState(val userList: List<Users> = listOf())
まとめ
今回、Roomをはじめて実装してみましたが、とても便利です。テーブル数が多くなった場合でも、Roomを使えば保守管理が行いやすそうです。パッケージのインストールに少し手間取りましたが、バージョンの互換性に気をつければクリアできる問題でした。 また、RoomやViewModelの実装部分で、アーキテクチャの部分でDIやコンテンア、ファクトリーメソッドなどの実装方法がとても勉強になりました。ぜひ Google Codelab Room を使用してデータを永続化する もご参考ください。
関連記事
- Composableを使ったListView表現【Android アプリ開発】
- はじめての Spring Boot 〜 JavaでWebアプリケーション
- Composableでテキストフィールドをつくる【Android アプリ開発】
- ESP32でHTTPSアクセス、ただし証明書検証なし
- ダイソーのBluetoothリモコンシャッターをESP32でハックする