まずはじめに

自分はAndroidアプリ初心者です。
なのでこの記事に多少のミスもあると思います。
あくまでサンプルですので、業務で使用しないで下さい。

ミスを批判・指摘される方は、正しいコードを載せたページを閲覧できる状態にして
このページの右ペインで公開しているTwitterアカウントにダイレクトメッセージいただけると、泣いて喜んで、このページの公開を停止します。

このページの最後にも追記あり。





目的

Androidアプリにて Jetpack ComposeキットでRoomを使った簡単なアプリを作ってみたいと思います。

環境

  • Android Studio Arctic Fox | 2020.3.1 Patch 3
  • Kotlin 203-1.5.31-release-550-AS7717.8




参照サイト

Roomライブラリについては以下の記事も参照下さい。





アプリの挙動

アプリ上のボタンをタップすると、タップした時刻をローカルで保存します。
アプリを再起動しても、次回起動時に前回にタップした時刻を表示するアプリです。





ソース全部

解説は特に書いていませんが、
参照サイト4つ目のドキュメントを見て、後は自分で考えました。
丸ごと載せているサイトって少ないのでは。

dependencies一覧

build.gradleのdependencies
dependencies {
    implementation 'androidx.core:core-ktx:1.7.0'
    implementation 'androidx.appcompat:appcompat:1.3.1'
    implementation 'com.google.android.material:material:1.4.0'
    implementation "androidx.compose.ui:ui:$compose_version"
    implementation "androidx.compose.material:material:$compose_version"
    implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
    implementation 'androidx.activity:activity-compose:1.3.0-alpha06'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
    androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
    debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"

    // LiveData
    def livedataVersion = "1.1.0-beta01"
    implementation "androidx.compose.runtime:runtime-livedata:$livedataVersion"
    def livedataKtxVersion = "2.4.0"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:$livedataKtxVersion"

    //room
    def roomVersion = "2.3.0"
    implementation("androidx.room:room-runtime:$roomVersion")
    annotationProcessor("androidx.room:room-compiler:$roomVersion")
    kapt("androidx.room:room-compiler:$roomVersion")
    implementation("androidx.room:room-ktx:$roomVersion")

}





Activity

MainActivity.kt
import android.icu.text.SimpleDateFormat
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.example.roomtest.ui.theme.RoomTestTheme
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import java.util.*

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val applicationScope = CoroutineScope(SupervisorJob())
        val database by lazy { AppDatabase.getDatabase(this, applicationScope) }
        val repository by lazy { AppRepository(database.loginDao()) }
        val viewModel: LoginViewModel by viewModels {
            LoginViewModelFactory(repository)
        }
        setContent {
            RoomTestTheme {
                Surface(color = MaterialTheme.colors.background) {
                    Content(viewModel)
                }
            }
        }
    }
}


@Composable
fun Content(viewModel: LoginViewModel) {
    val registDate = viewModel.loginItem.observeAsState()
    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Button(
            modifier = Modifier.padding(top = 64.dp),
            onClick = {
                val currentDate: String = SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.getDefault()).format(Date())
                val loginData = Login(
                    1,
                    currentDate
                )
                viewModel.update(loginData)
            }
        ) {
            Text(text = "regist")
        }

        Text(
            text = registDate.value?.logindate ?: "",
            modifier = Modifier.padding(top = 16.dp)
        )
    }
}





Database

AppDatabase.kt
import android.content.Context
import android.icu.text.SimpleDateFormat
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteDatabase
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import java.util.*

@Database(entities = [Login::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
    abstract fun loginDao(): LoginDao

    companion object {
        @Volatile
        private var INSTANCE: AppDatabase? = null

        fun getDatabase(context: Context, scope: CoroutineScope): AppDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    AppDatabase::class.java,
                    "app_database"
                )
                    .addCallback(LoginDatabaseCallback(scope))
                    .build()
                INSTANCE = instance
                instance
            }
        }

        private class LoginDatabaseCallback(
            private val scope: CoroutineScope
        ) : RoomDatabase.Callback() {
            override fun onCreate(db: SupportSQLiteDatabase) {
                super.onCreate(db)
                INSTANCE?.let { database ->
                    scope.launch {
                        val loginDao = database.loginDao()
                        val currentDate: String = SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.getDefault()).format(
                            Date()
                        )
                        val loginData = Login(
                            1,
                            currentDate
                        )
                        loginDao.insertLogin(loginData)
                    }
                }
            }
        }
    }
}





Repository
このRepositoryという使い方、概念は今回始めて知った。
情報の取得をローカルかネットからかで分ける時に使われる事が多いらしい。
使い方が誤っているというか、もっと良い使い方あるのか、今回の事例だと使わなくてよいのかもしれません。

AppRepository.kt
import androidx.annotation.WorkerThread
import kotlinx.coroutines.flow.Flow

class AppRepository(private val loginDao: LoginDao) {
    val loginData: Flow<Login> = loginDao.getLogin()

    @Suppress("RedundantSuspendModifier")
    @WorkerThread
    suspend fun update(login: Login) {
        loginDao.updateLogin(login)
    }
}

Entity

Login.kt
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "login_table")
data class Login(
    @PrimaryKey val uid: Int,
    @ColumnInfo(name = "logindate") val logindate: String?
)

Dao

LoginDao.kt
import androidx.room.*
import kotlinx.coroutines.flow.Flow

@Dao
interface LoginDao {
    @Query("SELECT * FROM login_table limit 1")
    fun getLogin(): Flow<Login>

    @Insert
    suspend fun insertLogin(vararg login: Login)

    @Update
    suspend fun updateLogin(vararg login: Login)
}





ViewModel

LoginViewModel.kt
import androidx.lifecycle.*
import kotlinx.coroutines.launch

class LoginViewModel(private val repository: AppRepository) : ViewModel() {
    var loginItem = repository.loginData.asLiveData()

    fun update(login: Login) = viewModelScope.launch {
        repository.update(login)
    }
}

class LoginViewModelFactory(private val repository: AppRepository) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(LoginViewModel::class.java)) {
            @Suppress("UNCHECKED_CAST")
            return LoginViewModel(repository) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}





誤った使用例があると思います。

所感としてはかなり難解。
LiveDataとかViewModelとかRepository部分とか。
Jetpack ComposeRoomを使ったリファレンスが少ないと思った。

このページは参照程度にとどめてください。

こうまで書かないと、Androidサンプルの場合、 サイトを全部コピペして。
それだけなら良いんだけど、 このサイトのせいで間違った使い方したと批判するワタシからしたらお馬鹿さんがいるんですよね。

自分も故意で誤った情報を公開するつもりはない。
そりゃ、誤った情報、誤解を与える書き方をしたこのサイトが悪いのも知っている。
そういうページはなるべく修正するように心がけている。

でも個人での技術ブログなんてクオリティたかが知れてるし、ドキュメントページじゃないわけだし。
そしてこのインターネットという環境においては、
安直に情報を鵜呑みにして、丸パクリで利用する方も悪いと思う。
そしてそして、責任転嫁するのだが、
批判するなら、大元のGoogleドキュメントのド圧倒的情報不足、いつまで経っても最新化しない、古い情報を修正しないことを訴えてくれ。