Android 项目 数据库

Android 项目 数据库

源码

配置

gradle/libs.versions.toml

[versions]
room = "2.8.4"
ksp = "2.3.5"

[libraries]
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }

[plugins]
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
room = { id = "androidx.room", version.ref = "room" }

app/build.gradle.kts

plugins {
    alias(libs.plugins.ksp)
    alias(libs.plugins.room)
}

room { schemaDirectory("$projectDir/schemas") }

dependencies {
    implementation(libs.androidx.room.runtime)
    implementation(libs.androidx.room.ktx)
    ksp(libs.androidx.room.compiler)
}

AppDatabase

package cn.com.xuxiaowei.data

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

/** @see Database.version 数据库版本,当表结构发生变化时,数字 +1 */
@Database(entities = [AppOpen::class], version = 1, exportSchema = true)
abstract class AppDatabase : RoomDatabase() {

    abstract fun appOpenDao(): AppOpenDao

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

        fun getDatabase(context: Context): AppDatabase {
            return Instance
                ?: synchronized(this) {
                    Room.databaseBuilder(context, AppDatabase::class.java, "app_database")
                        .fallbackToDestructiveMigration(false)
                        .build()
                        .also { Instance = it }
                }
        }
    }
}

AppOpen

package cn.com.xuxiaowei.data

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

@Entity(tableName = "app_open")
data class AppOpen(@PrimaryKey(autoGenerate = true) val id: Long = 0, val openedAt: Long)

AppOpenDao

package cn.com.xuxiaowei.data

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

@Dao
interface AppOpenDao {
    @Insert suspend fun insert(event: AppOpen)

    @Query("SELECT COUNT(*) FROM app_open") suspend fun count(): Long
}

RepoDbProvider

package cn.com.xuxiaowei.data.db

import android.content.Context

object RepoDbProvider {
    fun init(context: Context) {}
}

App

package cn.com.xuxiaowei

import android.app.Application
import android.os.Build
import cn.com.xuxiaowei.data.AppDatabase
import cn.com.xuxiaowei.data.AppOpen
import cn.com.xuxiaowei.data.db.RepoDbProvider
import cn.com.xuxiaowei.service.MinuteTaskService
import cn.com.xuxiaowei.utils.FileLogger
import cn.com.xuxiaowei.worker.FifteenMinuteWorker
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch

class App : Application() {
    private val applicationScope: CoroutineScope by lazy {
        CoroutineScope(SupervisorJob() + Dispatchers.IO)
    }

    override fun onCreate() {
        super.onCreate()

        RepoDbProvider.init(this)

        val appVersion =
            try {
                val pm = packageManager
                val pi = pm.getPackageInfo(packageName, 0)
                val versionName = pi.versionName ?: "unknown"
                val versionCode =
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                        pi.longVersionCode.toString()
                    } else {
                        @Suppress("DEPRECATION") pi.versionCode.toString()
                    }
                "$versionName($versionCode)"
            } catch (e: Exception) {
                "unknown"
            }

        val deviceInfo = "model=${Build.MODEL}, brand=${Build.BRAND}, sdk=${Build.VERSION.SDK_INT}"
        FileLogger.log(this, "startup", "App", "App started version=$appVersion, $deviceInfo")

        applicationScope.launch {
            val db = AppDatabase.getDatabase(this@App)
            db.appOpenDao().insert(AppOpen(openedAt = System.currentTimeMillis()))
            val count = db.appOpenDao().count()
            FileLogger.log(this@App, "startup", "App", "App open count=$count")
        }

        MinuteTaskService.start(this)
        FifteenMinuteWorker.schedule(this)
    }
}

效果

数据库迁移

AppOpen

package cn.com.xuxiaowei.data

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

@Entity(tableName = "app_open")
data class AppOpen(
    @PrimaryKey(autoGenerate = true) val id: Long = 0,
    val openedAt: Long,
    val appVersion: String? = null,
)

AppDatabase

package cn.com.xuxiaowei.data

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase

/** @see Database.version 数据库版本,当表结构发生变化时,数字 +1 */
@Database(entities = [AppOpen::class], version = 2, exportSchema = true)
abstract class AppDatabase : RoomDatabase() {

    abstract fun appOpenDao(): AppOpenDao

    companion object {
        @Volatile private var Instance: AppDatabase? = null
        private val MIGRATION_1_2 =
            object : Migration(1, 2) {
                override fun migrate(db: SupportSQLiteDatabase) {
                    db.execSQL("ALTER TABLE app_open ADD COLUMN app_version TEXT")
                }
            }

        fun getDatabase(context: Context): AppDatabase {
            return Instance ?: synchronized(this) {
                Room.databaseBuilder(context, AppDatabase::class.java, "app_database")
                    .addMigrations(MIGRATION_1_2)
                    .fallbackToDestructiveMigration(false)
                    .build()
                    .also { Instance = it }
            }
        }
    }
}

App

package cn.com.xuxiaowei

import android.app.Application
import android.os.Build
import cn.com.xuxiaowei.data.AppDatabase
import cn.com.xuxiaowei.data.AppOpen
import cn.com.xuxiaowei.data.db.RepoDbProvider
import cn.com.xuxiaowei.service.MinuteTaskService
import cn.com.xuxiaowei.utils.FileLogger
import cn.com.xuxiaowei.worker.FifteenMinuteWorker
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch

class App : Application() {
    private val applicationScope: CoroutineScope by lazy {
        CoroutineScope(SupervisorJob() + Dispatchers.IO)
    }

    override fun onCreate() {
        super.onCreate()

        RepoDbProvider.init(this)

        val appVersion =
            try {
                val pm = packageManager
                val pi = pm.getPackageInfo(packageName, 0)
                val versionName = pi.versionName ?: "unknown"
                val versionCode =
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                        pi.longVersionCode.toString()
                    } else {
                        @Suppress("DEPRECATION") pi.versionCode.toString()
                    }
                "$versionName($versionCode)"
            } catch (e: Exception) {
                "unknown"
            }

        val deviceInfo = "model=${Build.MODEL}, brand=${Build.BRAND}, sdk=${Build.VERSION.SDK_INT}"
        FileLogger.log(this, "startup", "App", "App started version=$appVersion, $deviceInfo")

        applicationScope.launch {
            val db = AppDatabase.getDatabase(this@App)
            db.appOpenDao()
                .insert(AppOpen(openedAt = System.currentTimeMillis(), appVersion = appVersion))
            val count = db.appOpenDao().count()
            FileLogger.log(this@App, "startup", "App", "App open count=$count")
        }

        MinuteTaskService.start(this)
        FifteenMinuteWorker.schedule(this)
    }
}