跪拜 Guibai
← Back to the summary

Room 3.0 Ships Under a New Package, Making KMP the Default Path

On July 1st, androidx.room3:room3-*:3.0.0 was officially released.

This is not a minor version upgrade from Room 2.x. Room 3.0 has moved to a new androidx.room3 package and Maven group, with Kotlin Multiplatform as its core direction. Meanwhile, old entry points like SupportSQLite, KAPT, and Java code generation have been moved out of the main path.

Image

Dependency Entry Point

The coordinates for Room 3.0 have changed from androidx.room to androidx.room3. If you simply change the version number from 2.x to 3.0.0, Gradle dependency resolution will fail to find the original artifact.

plugins {
    id("com.google.devtools.ksp")
    id("androidx.room3") version "3.0.0"
}

dependencies {
    val roomVersion = "3.0.0"

    implementation("androidx.room3:room3-runtime:$roomVersion")
    ksp("androidx.room3:room3-compiler:$roomVersion")
}

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

Two points must not be missed here. First, Room 3.0 requires KSP; you can no longer use KAPT or the Java annotation processor to configure room3-compiler. Second, the module must configure the com.google.devtools.ksp plugin, and the KSP version still needs to align with the project's Kotlin version.

Package names must also be changed. androidx.room.RoomDatabase becomes androidx.room3.RoomDatabase. Core annotations like @Database, @Entity, @Dao, and @Query still exist, only their import paths have changed. Room 2.x and Room 3.0 can coexist because of the different package names, which reduces conflicts brought by transitive dependencies like WorkManager.

Image

New Package Name

The new package name is not simply to "look like a major version." In the Room 2.x ecosystem, many libraries transitively depend on androidx.room. If Room 3.0 still used the old package name, migration would easily lead to mixing ABI, annotation processor, and runtime code issues.

After switching to androidx.room3, old code can remain on androidx.room, while new modules separately connect to androidx.room3. This is more practical for large projects: you can first migrate a database centralized in a data module, rather than changing the entire repository at once.

// Room 2.x
import androidx.room.Database
import androidx.room.RoomDatabase

// Room 3.0
import androidx.room3.Database
import androidx.room3.RoomDatabase

If the project has common wrappers, such as AppDatabaseProvider, RoomModule, or DatabaseFactory, start by changing imports and builders in these locations. If business-layer DAO calls originally only exposed suspend / Flow, the changes will be much fewer; if the business layer obtained SupportSQLiteDatabase or Cursor, it will need to be broken down more granularly later.

SQLiteDriver

The database bottom layer of Room 3.0 has been changed to SQLiteDriver. The main API no longer references SupportSQLiteDatabase or SupportSQLiteOpenHelper, and no longer places Android's Cursor in the core path. This change directly serves KMP, because common code cannot depend on Android-specific types.

The most common direct transaction writing method must be changed to a suspend API:

// Room 2.x
database.runInTransaction {
    dao.insert(user)
    dao.updateCount(user.id)
}

// Room 3.0
database.withWriteTransaction {
    dao.insert(user)
    dao.updateCount(user.id)
}

Direct queries also no longer return a Cursor. Room 3.0 provides connections and statements:

database.useReaderConnection { connection ->
    connection.usePrepared("SELECT name FROM User WHERE id = ?") { stmt ->
        stmt.bindLong(1, userId)
        if (stmt.step()) {
            stmt.getText(0)
        } else {
            null
        }
    }
}

Migration callbacks have also changed. Previously, SupportSQLiteDatabase was common in Migration.migrate() and RoomDatabase.Callback.onCreate(). In Room 3.0, the corresponding type is SQLiteConnection. If the repository has hand-written SQL, index creation, default value patching, or data repair scripts, these callbacks are the most likely places to report errors first during migration.

Image

Compatibility Wrapper

Room 3.0 has not completely blocked SupportSQLite. If the project still has third-party tools or old wrappers that can only consume SupportSQLiteDatabase, you can temporarily add androidx.room3:room3-sqlite-wrapper.

dependencies {
    implementation("androidx.room3:room3-sqlite-wrapper:3.0.0")
}

Code that previously obtained a writable database from openHelper can be changed to use the wrapper:

// Room 2.x
val db = database.openHelper.writableDatabase

// Room 3.0
val db = database.getSupportWrapper()

This artifact is more of a migration buffer. New code should still move towards SQLiteDriver and coroutine APIs; otherwise, it's just wrapping the old dependency, and you will still hit Android type boundaries when connecting to KMP, Web, or common code later.

Coroutine-First

Room 3.0 requires database operations to use coroutines. If a DAO function does not return Flow, PagingSource, or a custom DAO return type, it needs to be suspend.

@Dao
interface UserDao {
    @Query("SELECT * FROM User WHERE id = :id")
    suspend fun findById(id: Long): User?

    @Query("SELECT * FROM User ORDER BY name")
    fun observeUsers(): Flow<List<User>>
}

The database builder no longer configures an Executor. If you need to specify a thread, change it to pass a CoroutineContext and dispatcher. This allows Room to use the same coroutine model on platforms like Android, iOS, desktop, and web.

The InvalidationTracker.Observer set has also been removed; addObserver() and removeObserver() are no longer the observation entry points for Room 3.0. The new approach is to get a Flow from InvalidationTracker.createFlow() and then combine query results in the business logic.

fun observeUserTable(): Flow<List<User>> {
    return database.invalidationTracker
        .createFlow("User")
        .map { userDao.loadAll() }
}

The key point of this code is not the extra layer of Flow, but that invalidation notifications and DAO queries both enter the coroutine model. If the project previously had manual observers, then manually switched threads, emitted LiveData or callbacks, this part can be consolidated into the Flow chain during migration.

Image

Paging Approach

In Room 2.x, return types like PagingSource, RxJava, Guava, and LiveData had built-in integration. Room 3.0 changes them to DAO return type converters, which need to be registered on @Dao or @Database.

Taking Paging as an example, the dependency is still added separately:

dependencies {
    implementation("androidx.room3:room3-paging:3.0.0")
}

PagingSourceDaoReturnTypeConverter must be explicitly registered on the DAO:

@Dao
@DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class)
interface SongDao {
    @Query("SELECT * FROM Song ORDER BY name")
    fun pagingSource(): PagingSource<Int, Song>
}

This change will affect existing paginated lists. Previously, simply returning PagingSource<Int, Entity> would compile; now, if the converter is missing, the Room compiler will report an unsupported return type. RxJava3, Guava, and LiveData are the same type of issue, with corresponding artifacts room3-rxjava3, room3-guava, and room3-livedata.

DAO return type converters can also be customized. Room-generated code is responsible for executing SQL and converting entities; the converter determines the final wrapped return type. If the web side needs to wrap a DAO return value as a Promise, it falls into this category of extension.

KMP and Web

Room 3.0's KMP-oriented changes are not limited to Android and iOS. It has added JavaScript and WasmJS targets. Combined with the SQLiteDriver interface in androidx.sqlite:sqlite and WebWorkerSQLiteDriver in androidx.sqlite:sqlite-web, Room can be placed into common code covering the Web.

The Web platform has one obvious limitation: database operations are inherently asynchronous. APIs in Room that receive SQLiteStatement will become suspend functions, such as Migration.onMigrate(), RoomDatabase.Callback.onCreate(), and PooledConnection.usePrepared(). If common code targets both Web and non-Web, prioritize using the suspend extension functions provided by androidx.sqlite, such as step() and executeSQL().

import androidx.sqlite.executeSQL
import androidx.sqlite.step

database.useWriterConnection { connection ->
    val count = connection.usePrepared("SELECT count(*) FROM Song") { stmt ->
        stmt.step()
        stmt.getLong(0)
    }
    connection.executeSQL("DELETE FROM Song")
    count
}

WebWorkerSQLiteDriver also requires a Web Worker that implements the communication protocol. Currently, it does not come with a default worker. The AndroidX repository has an example worker that can combine SQLite WASM and OPFS storage. This means the Web direction is already runnable, but it is not a feature that can be casually enabled by just changing a dependency in an Android project.

Image

Table Structure Capabilities

Room 3.0 has also supplemented some SQLite-level capabilities. @Fts5 can declare FTS5 tables, primary keys have added generation algorithm configuration, and WITHOUT ROWID tables can also be created.

@Fts5
@Entity(tableName = "ArticleFts")
data class ArticleFts(
    @PrimaryKey
    val rowid: Long,
    val title: String,
    val body: String,
)

Kotlin default parameter values can also be recognized by Room. Previously, when entity fields had default values, complete constructor arguments were often still required in many scenarios. Room 3.0 can interface with Kotlin data classes more naturally.

@Entity
data class DownloadTask(
    @PrimaryKey val id: String,
    val url: String,
    val retryCount: Int = 0,
    val state: String = "pending",
)

These capabilities do not necessarily require immediate migration, but they indicate that Room 3.0's focus has shifted more towards Kotlin and multiplatform. For Android-only projects with a thin database wrapper, the upgrade cost mainly lies in imports, KSP, coroutines, and the SQLite API; if the database layer has a lot of historical baggage, points like SupportSQLiteDatabase, Cursor, KAPT, Executor, and Observer must all be addressed one by one.

Finally

If a project only uses Room as an Android local database, there is no need to rush to upgrade to 3.0; if you are already building a KMP data layer, Room 3.0 is the main line to align with going forward.

#Android #Jetpack #Room #KotlinMultiplatform

Comments

Top 1 from juejin.cn, machine-translated. The original thread is authoritative.

lotosbin同学

👍