Androidアプリ開発でRoomを学ぶ者

「アプリでデータを保存したい・・・!」という思いの元、いろいろ調べた結果、
データを登録するならRoomライブラリが必要という結論に至りました。

初めてAndroid開発をしようとしていた7,8年前にはなかった気がします。

ひとまずログを保存できるようにしたいという目的があり、
今回操作ログを残すための準備として、どうやってRoomライブラリを使って保存するかの検証していきます。

スポンサーリンク

ファイル群

Entityクラス/com/example/roomtest2/data/db/Log.ktテーブル定義を記載する
⇒@Entity、@PrimaryKeyなど
DAOクラス/com/example/roomtest2/data/db/LogDao.ktテーブル操作を定義する
@Dao、@Insert、@Queryなど
Databaseクラス/com/example/roomtest2/data/db/DataBase.ktアプリで取り扱うスキーマと管理情報を定義する
@Databaseなど
MainActivityクラス/com/example/roomtest2/MainActivity.ktテスト用に改修
INSERT命令クラス/com/example/roomtest2/db/AppViewModel.ktテスト用に準備
レイアウトファイル/res/layout/activity_main.xmlテスト用に改修

※アプリ名はroomtest2として作成

必要なソースコードの個別説明(ログを記録するための主要部分)

build.gradle.kts

■dependenciesに追加する内容

dependencies {
    val room_version = "2.8.4"
    implementation("androidx.room:room-runtime:$room_version") //Room基本ライブラリ
    implementation("androidx.room:room-ktx:$room_version") //Room拡張機能
    ksp("androidx.room:room-compiler:$room_version") //Roomコード生成コンパイラ

    //▼以下はログを追加テストするボタン用
    val lifecycle_version = "2.10.0"
    implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version")
    implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version")

    val coroutines_version = "1.8.1"
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version")
}

kspの記述で「Unresolved reference 'ksp'」と警告が出る場合、後述のpluginsの追加をしてSyncNowすればOK
※キャプチャのバージョン古い検証時のままになってしまった💦

■pluginsに追加する内容

plugins{
    id("com.google.devtools.ksp") version "2.0.21-1.0.26" //kspプラグイン読み込み
}

※外部プラグインのためversion指定は必須。

■dependencies、pluginsを指定後Sync実行を実行する

ん?、さっきから所々バージョン決め打ちしてるけどこれは大丈夫?

バージョンについて

この記事作成時に調べたバージョンを指定しているため、
将来的に見返した時これらが古くなっている可能性があります。

じゃあどうするの・・・?
ってことで下記の手順でバージョンを確認していきます。

■pluginsに設定するバージョン

本記事上は「version "2.0.21-1.0.26"」としています。
バージョンは-で区切られていて、「Kotlinバージョン - KSPバージョン」になります。

それぞれ下記の手順で確認していけばOKです。

1.「libs.versions.toml」でkotlinバージョンを確認

※今回は2.0.21であることを確認

2.maven central repositoryで対応するKSPバージョンを確認

maven central repositoryのVersionsタブで推奨バージョンを確認します。
「VERSION NUMBER」列にあるもので開発中のkotlinバージョンと一致するものを探します。

今回はKotlin”2.0.21”なので、それ以降対応しているいずれかのKSPバージョンを使います。

■dependenciesに設定するバージョン

ん~分からない!!

調べてみてもroomのバージョンをいくつにするかの明確な情報が出てきませんでした。。。

何かないかといろいろ考えた結果、Developerページの内容が安定板じゃないか?
という結論に至りました。※実務やってないからここは自信ないです

Log.kt

ログを格納するテーブルのdataクラス。
アノテーションでテーブル名、フィールドで列名を指定する。


■dataクラスとは
・テーブルのスキーマを定義する
・同時にテーブルの1行分のデータを表す

例えばINSERTしたい場合は下記の手順で行う。
1.当該インスタンスを生成
2.フィールドに登録したいデータを代入
3.Daoクラスのinsert用関数にイそのインスタンスを渡すと、そのまま1行として登録される

package com.example.roomtest2.data.db

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "logs")
data class Log(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0, //必須
    val timestamp: Long = System.currentTimeMillis(), //必須
    val title: String, //必須
    val action: String, //必須
    val detail: String? = null //?が付いているため任意扱い
)

@Entityでテーブル名を指定する。指定されたテーブル名をもとにRoomがテーブルを作成する。
 省略した場合、クラス名をテーブル名として扱う。

@PrimaryKeyで主キーを指定する。単一・複合主キーはそれぞれ記載する場所が異なる。
 ⇒単一主キーの場合:クラス内の主キーとしたいフィールドにアノテーションとして記載する
 ⇒複合主キーの場合:クラスの外に記載する ※@Entity(primaryKeys = ["id", "timestamp"])など
 ※(autoGenerate = true)は主キーを自動採番にする

・timestamp列はunixタイムスタンプをデフォルト登録

LogDao.kt

DAOクラス
DBを操作するため、アプリケーションからの命令を仲介する。

package com.example.roomtest2.data.db

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query

@Dao
interface LogDao {
    @Insert
    suspend fun insert(log: Log)

    @Query("SELECT * FROM logs ORDER BY timestamp DESC")
    suspend fun getLatest(): List<Log>

    @Query("DELETE FROM logs  WHERE timestamp < :limit")
    suspend fun deleteLogLimit(limit:Long)
}

・@DaoのアノテーションによってRoomがDAOとして認識する

・各関数ごとにSQLをアノテーションで指定する
 ⇒@Insert:INSERTを実行する
 ⇒@Query:アノテーションで指定したSQLを実行する

Database.kt

Room用DB管理クラス

package com.example.roomtest2.data.db

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase

@Database(
    entities = [
        Log::class,//複数ある場合は必要なだけ列挙
    ],
    version = 1, //スキーマ構造の変更が必要な場合はカウントアップし、マイグレーションを促す
    exportSchema = true //バージョンごとにスキーマJSONを保存する、保存先はgradleで指定する
)

abstract class DataBase : RoomDatabase(){
    abstract fun LogDao(): LogDao
    companion object {
        @Volatile //スレッド間で必ず最新の変数を参照
        private var INSTANCE: DataBase? = null

        fun getInstance(context: Context): DataBase {
            //INSTANCEが空であれば初期化する
            return INSTANCE ?: synchronized(this) {

                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    DataBase::class.java,
                    "log_database.db" //.dbは必須ではない
                ).build()
                INSTANCE = instance
                instance
            }
        }
    }
}

・@Databaseアノテーションにより、アプリで取り扱うスキーマと管理情報を指定する

・クラスはabstractにし、RoomDatabaseを継承する

・関数は取り扱いたいDaoクラス型を戻り値とし、必要な数だけabstractで宣言する

・companion object(Javaのstatic扱い)でインスタンス化不要の領域を宣言する

必要なソースコードの個別説明(ログを記録するテスト部分)

ログを記録するためのデータベースは前述で準備できたので、
データベースに登録するためのテスト処理を用意します。

今回はボタンを押したら定型文が登録されるような簡単なものにします。

activity_main.xml

レイアウトにButtonを追加。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/addButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="ログを追加する"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.109" />
</androidx.constraintlayout.widget.ConstraintLayout>

AppViewModel.kt

ボタンを追加したときにログをINSERTする処理を記述

package com.example.roomtest2.data

import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.example.roomtest2.data.db.DataBase
import com.example.roomtest2.data.db.Log
import com.example.roomtest2.data.db.LogDao

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class AppViewModel(app: Application) : AndroidViewModel(app) {

    private lateinit var dao: LogDao
    //コンストラクタ
    init{
        val db = DataBase.getInstance(app)
        dao = db.LogDao()
    }

    fun onLogButtonClicked(){
        viewModelScope.launch { (Dispatchers.IO)
            //INSERT用のデータを作成
            val log = Log(
                title = "test" + System.currentTimeMillis().toString(),
                action = "追加した",
                detail = "詳しい処理を記載"

            )
            //INSERT実行
            dao.insert(log)
        }
    }
}

MainActivity.kt

レイアウトに追加したボタンにイベントリスナーを登録し、INSERTを実行する関数を呼び出す。
※確認用にToastでボタンが推せたことを確認できるようにする。

package com.example.roomtest2

import android.os.Bundle
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.example.roomtest2.data.AppViewModel
import com.example.roomtest2.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    private val appViewModel: AppViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        //setContentView(R.layout.activity_main)
        //ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
        //    val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
        //    v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
        //    insets
        //}
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.addButton.setOnClickListener {
            appViewModel.onLogButtonClicked()
            Toast.makeText(this,"押した",Toast.LENGTH_SHORT).show()
        }
    }
}

※ActivityMainBindingというクラスと、activity_main.xmlのレイアウトファイルが名前によって自動的に紐づけされっる様になっている

動作確認した結果

ボタンを押した後はデータが登録されているかを確認します。
AndroidStudioの「App Inspection」から今回作成した「logs」テーブルを開けば登録されていることが確認できる。
※もし「App Inspectionが見つからない場合は、メニューから「View>Tool Windows>App Inspection」を探す

まとめ

はじめは軽くINSERTするだけでも結構大変だな、、、と思ったものの、
Entity、Dao、DataBaseファイルそれぞれをきちんと見ていくとシンプルで、
「ああ、なるほど」と思うような構成でした。

やっぱりChatGPTとかのチャットAIが強いですね、、無かったら結構大変だったかも。
App Inspectionでデータ登録を確認できた時は感動しました、思わずガッツポーズ!
初めて動いたときの喜びは色褪せない・・・!

スポンサーリンク
おすすめの記事