Androidアプリ開発 Compose x Room を使って Database を操作する
以前に ROOMを使わずにSQLiteを扱う、Androidアプリ開発 の記事で、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 ブロックに以下を追加します:
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"]}") // 追加
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0") // ViewModelのために追加
プロジェクト レベルの Gradle ファイル build.gradle.kts を開いて以下を追加します:
buildscript {
extra.apply {
set("room_version", "2.6.1")
}
}
KSPプラグインのインストール
KSP(Kotlin Symbol Processing) とは、Kotlinコードを処理するためのツールで、アノテーションプロセッサの一種です。Roomを使う際にKSPが必要となります。少しつまずきやすいのでバージョンなどに気をつけながらインストールしてください。KSPをインストールするためにプpluginsに以下を追加します:
plugins {
...
id("com.google.devtools.ksp") version "1.9.0-1.0.13"
}
今回使用する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
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
// jvmTarget = "1.8"
jvmTarget = "17"
}
以上で、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,
name TEXT NOT NULL,
age INTEGER NOT NULL
);
【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)
suspend fun insert(item: Users)
@Update
suspend fun update(item: Users)
@Delete
suspend fun delete(item: Users)
@Query("SELECT * from users WHERE id = :id")
fun getItem(id: Int): Flow<Users>
@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) {
Room.databaseBuilder(context, MyDatabase::class.java, "my_database").build().also { INSTANCE = it }
}
}
}
}
【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 {
OfflineUsersRepository(MyDatabase.getDatabase(context).usersDao())
}
}
【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()
container = AppDataContainer(this)
}
}
クラスを作成したら、マニフェストの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 {
HomeViewModel(inventoryApplication().container.usersRepository)
}
}
}
/**
* 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(
text = "ROOMテスト",
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(16.dp)
)
},
floatingActionButton = {
FloatingActionButton(
onClick = {viewModel.addUser()},
shape = MaterialTheme.shapes.medium,
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "追加"
)
}
},
) { innerPadding ->
LazyColumn(content = {
items(items = homeUiState.userList, key = { it.id }) { user ->
HomeListItem(
user = user,
modifier = Modifier
.clickable {
viewModel.deleteUser(user)
}
)
}
},
modifier = Modifier
.padding(innerPadding)
)
}
}
@Composable
fun HomeListItem(user: Users, modifier: Modifier = Modifier) {
Text(
text = "${user.id} : ${user.name} (${user.age})",
modifier = modifier.padding(16.dp)
)
}
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> =
usersRepository.getAllUsers().map { HomeUiState(it) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS),
initialValue = HomeUiState()
)
companion object {
private const val TIMEOUT_MILLIS = 5_000L
}
fun addUser() {
viewModelScope.launch {
val newUser = Users(name = "New User", age = 30)
usersRepository.insertUser(newUser)
}
}
fun deleteUser(user: Users) {
viewModelScope.launch {
usersRepository.deleteUser(user)
}
}
}
/**
* Ui State for HomeScreen
*/
data class HomeUiState(val userList: List<Users> = listOf())
まとめ
今回、Roomをはじめて実装してみましたが、とても便利です。テーブル数が多くなった場合でも、Roomを使えば保守管理が行いやすそうです。パッケージのインストールに少し手間取りましたが、バージョンの互換性に気をつければクリアできる問題でした。 また、RoomやViewModelの実装部分で、アーキテクチャの部分でDIやコンテンア、ファクトリーメソッドなどの実装方法がとても勉強になりました。ぜひ Google Codelab Room を使用してデータを永続化する もご参考ください。