Merge room-ktx into room-runtime
And very importantly add Coroutines as a dependency on room-runtime.
Also add an explicit dep to room-ktx to certain projects since atomic group constraints are not enforced via project() dependencies.
Bug: 317120607
Relnote: "RoomDatabase Kotlin extensions, withTransaction(), invalidationTrackerFlow() and others have been moved from `room-ktx` to `room-runtime`. The artifact `room-ktx` is now empty and is not needed for Coroutines support in Room, and specifically not needed when defining suspend functions in DAOs."
Test: ./gradlew checkApi
Change-Id: Ib7619848956632f51608db6d3e0f40f6782c6251
diff --git a/benchmark/integration-tests/macrobenchmark-target/build.gradle b/benchmark/integration-tests/macrobenchmark-target/build.gradle
index d455803..fbe028a 100644
--- a/benchmark/integration-tests/macrobenchmark-target/build.gradle
+++ b/benchmark/integration-tests/macrobenchmark-target/build.gradle
@@ -45,4 +45,5 @@
implementation(project(":work:work-runtime"))
implementation(project(":work:work-runtime-ktx"))
implementation(project(":room:room-runtime"))
+ implementation(project(":room:room-ktx"))
}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt
index a737a73..5942d89 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt
@@ -111,11 +111,6 @@
): MethodProcessorDelegate {
val asMember = executableElement.asMemberOf(containing)
return if (asMember.isSuspendFunction()) {
- val hasCoroutineArtifact = context.processingEnv
- .findTypeElement(COROUTINES_ROOM.canonicalName) != null
- if (!hasCoroutineArtifact) {
- context.logger.e(ProcessorErrors.MISSING_ROOM_COROUTINE_ARTIFACT)
- }
SuspendMethodProcessorDelegate(
context,
containing,
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
index 1bea64a..d4cbabc 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
@@ -653,9 +653,6 @@
"add `room-paging-rxjava3` artifact from Room as a dependency. " +
"androidx.room:room-paging-rxjava3:<version>"
- val MISSING_ROOM_COROUTINE_ARTIFACT = "To use Coroutine features, you must add `ktx`" +
- " artifact from Room as a dependency. androidx.room:room-ktx:<version>"
-
fun ambiguousConstructor(
pojo: String,
paramName: String,
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/binderprovider/CoroutineFlowResultBinderProvider.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/binderprovider/CoroutineFlowResultBinderProvider.kt
index ddd7285..932998d 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/binderprovider/CoroutineFlowResultBinderProvider.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/binderprovider/CoroutineFlowResultBinderProvider.kt
@@ -18,7 +18,6 @@
import androidx.room.compiler.processing.XType
import androidx.room.ext.KotlinTypeNames
-import androidx.room.ext.RoomCoroutinesTypeNames.COROUTINES_ROOM
import androidx.room.parser.ParsedQuery
import androidx.room.processor.Context
import androidx.room.processor.ProcessorErrors
@@ -27,17 +26,7 @@
import androidx.room.solver.query.result.CoroutineFlowResultBinder
import androidx.room.solver.query.result.QueryResultBinder
-@Suppress("FunctionName")
-fun CoroutineFlowResultBinderProvider(context: Context): QueryResultBinderProvider =
- CoroutineFlowResultBinderProviderImpl(
- context
- ).requireArtifact(
- context = context,
- requiredType = COROUTINES_ROOM,
- missingArtifactErrorMsg = ProcessorErrors.MISSING_ROOM_COROUTINE_ARTIFACT
- )
-
-private class CoroutineFlowResultBinderProviderImpl(
+class CoroutineFlowResultBinderProvider(
val context: Context
) : QueryResultBinderProvider {
companion object {
diff --git a/room/room-ktx/api/current.ignore b/room/room-ktx/api/current.ignore
new file mode 100644
index 0000000..558a9f0
--- /dev/null
+++ b/room/room-ktx/api/current.ignore
@@ -0,0 +1,5 @@
+// Baseline format: 1.0
+RemovedPackage: androidx.room:
+ Removed package androidx.room
+RemovedPackage: androidx.room.migration:
+ Removed package androidx.room.migration
diff --git a/room/room-ktx/api/current.txt b/room/room-ktx/api/current.txt
index ceaca54..e6f50d0 100644
--- a/room/room-ktx/api/current.txt
+++ b/room/room-ktx/api/current.txt
@@ -1,18 +1 @@
// Signature format: 4.0
-package androidx.room {
-
- public final class RoomDatabaseKt {
- method public static kotlinx.coroutines.flow.Flow<java.util.Set<java.lang.String>> invalidationTrackerFlow(androidx.room.RoomDatabase, String[] tables, optional boolean emitInitialState);
- method public static suspend <R> Object? withTransaction(androidx.room.RoomDatabase, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super R>);
- }
-
-}
-
-package androidx.room.migration {
-
- public final class MigrationKt {
- method public static androidx.room.migration.Migration Migration(int startVersion, int endVersion, kotlin.jvm.functions.Function1<? super androidx.sqlite.db.SupportSQLiteDatabase,kotlin.Unit> migrate);
- }
-
-}
-
diff --git a/room/room-ktx/api/restricted_current.ignore b/room/room-ktx/api/restricted_current.ignore
new file mode 100644
index 0000000..558a9f0
--- /dev/null
+++ b/room/room-ktx/api/restricted_current.ignore
@@ -0,0 +1,5 @@
+// Baseline format: 1.0
+RemovedPackage: androidx.room:
+ Removed package androidx.room
+RemovedPackage: androidx.room.migration:
+ Removed package androidx.room.migration
diff --git a/room/room-ktx/api/restricted_current.txt b/room/room-ktx/api/restricted_current.txt
index fc6c3c0..e6f50d0 100644
--- a/room/room-ktx/api/restricted_current.txt
+++ b/room/room-ktx/api/restricted_current.txt
@@ -1,31 +1 @@
// Signature format: 4.0
-package androidx.room {
-
- @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class CoroutinesRoom {
- method public static <R> kotlinx.coroutines.flow.Flow<R> createFlow(androidx.room.RoomDatabase db, boolean inTransaction, String[] tableNames, java.util.concurrent.Callable<R> callable);
- method public static suspend <R> Object? execute(androidx.room.RoomDatabase db, boolean inTransaction, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Callable<R> callable, kotlin.coroutines.Continuation<? super R>);
- method public static suspend <R> Object? execute(androidx.room.RoomDatabase db, boolean inTransaction, java.util.concurrent.Callable<R> callable, kotlin.coroutines.Continuation<? super R>);
- field public static final androidx.room.CoroutinesRoom.Companion Companion;
- }
-
- public static final class CoroutinesRoom.Companion {
- method public <R> kotlinx.coroutines.flow.Flow<R> createFlow(androidx.room.RoomDatabase db, boolean inTransaction, String[] tableNames, java.util.concurrent.Callable<R> callable);
- method public suspend <R> Object? execute(androidx.room.RoomDatabase db, boolean inTransaction, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Callable<R> callable, kotlin.coroutines.Continuation<? super R>);
- method public suspend <R> Object? execute(androidx.room.RoomDatabase db, boolean inTransaction, java.util.concurrent.Callable<R> callable, kotlin.coroutines.Continuation<? super R>);
- }
-
- public final class RoomDatabaseKt {
- method public static kotlinx.coroutines.flow.Flow<java.util.Set<java.lang.String>> invalidationTrackerFlow(androidx.room.RoomDatabase, String[] tables, optional boolean emitInitialState);
- method public static suspend <R> Object? withTransaction(androidx.room.RoomDatabase, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super R>);
- }
-
-}
-
-package androidx.room.migration {
-
- public final class MigrationKt {
- method public static androidx.room.migration.Migration Migration(int startVersion, int endVersion, kotlin.jvm.functions.Function1<? super androidx.sqlite.db.SupportSQLiteDatabase,kotlin.Unit> migrate);
- }
-
-}
-
diff --git a/room/room-ktx/src/main/java/androidx/room/RoomDatabaseExt.kt b/room/room-ktx/src/main/java/androidx/room/RoomDatabaseExt.kt
deleted file mode 100644
index 13b2b53..0000000
--- a/room/room-ktx/src/main/java/androidx/room/RoomDatabaseExt.kt
+++ /dev/null
@@ -1,238 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-@file:JvmName("RoomDatabaseKt")
-
-package androidx.room
-
-import androidx.annotation.RestrictTo
-import java.util.concurrent.RejectedExecutionException
-import java.util.concurrent.atomic.AtomicBoolean
-import java.util.concurrent.atomic.AtomicInteger
-import kotlin.coroutines.ContinuationInterceptor
-import kotlin.coroutines.CoroutineContext
-import kotlin.coroutines.coroutineContext
-import kotlin.coroutines.resume
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.asContextElement
-import kotlinx.coroutines.awaitCancellation
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.callbackFlow
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.suspendCancellableCoroutine
-import kotlinx.coroutines.withContext
-
-/**
- * Calls the specified suspending [block] in a database transaction. The transaction will be
- * marked as successful unless an exception is thrown in the suspending [block] or the coroutine
- * is cancelled.
- *
- * Room will only perform at most one transaction at a time, additional transactions are queued
- * and executed on a first come, first serve order.
- *
- * Performing blocking database operations is not permitted in a coroutine scope other than the
- * one received by the suspending block. It is recommended that all [Dao] function invoked within
- * the [block] be suspending functions.
- *
- * The internal dispatcher used to execute the given [block] will block an utilize a thread from
- * Room's transaction executor until the [block] is complete.
- */
-public suspend fun <R> RoomDatabase.withTransaction(block: suspend () -> R): R {
- val transactionBlock: suspend CoroutineScope.() -> R = transaction@{
- val transactionElement = coroutineContext[TransactionElement]!!
- transactionElement.acquire()
- try {
- @Suppress("DEPRECATION")
- beginTransaction()
- try {
- val result = block.invoke()
- @Suppress("DEPRECATION")
- setTransactionSuccessful()
- return@transaction result
- } finally {
- @Suppress("DEPRECATION")
- endTransaction()
- }
- } finally {
- transactionElement.release()
- }
- }
- // Use inherited transaction context if available, this allows nested suspending transactions.
- val transactionDispatcher = coroutineContext[TransactionElement]?.transactionDispatcher
- return if (transactionDispatcher != null) {
- withContext(transactionDispatcher, transactionBlock)
- } else {
- startTransactionCoroutine(coroutineContext, transactionBlock)
- }
-}
-
-/**
- * Suspend caller coroutine and start the transaction coroutine in a thread from the
- * [RoomDatabase.transactionExecutor], resuming the caller coroutine with the result once done.
- * The [context] will be a parent of the started coroutine to propagating cancellation and release
- * the thread when cancelled.
- */
-private suspend fun <R> RoomDatabase.startTransactionCoroutine(
- context: CoroutineContext,
- transactionBlock: suspend CoroutineScope.() -> R
-): R = suspendCancellableCoroutine { continuation ->
- try {
- transactionExecutor.execute {
- try {
- // Thread acquired, start the transaction coroutine using the parent context.
- // The started coroutine will have an event loop dispatcher that we'll use for the
- // transaction context.
- runBlocking(context.minusKey(ContinuationInterceptor)) {
- val dispatcher = coroutineContext[ContinuationInterceptor]!!
- val transactionContext = createTransactionContext(dispatcher)
- continuation.resume(
- withContext(transactionContext, transactionBlock)
- )
- }
- } catch (ex: Throwable) {
- // If anything goes wrong, propagate exception to the calling coroutine.
- continuation.cancel(ex)
- }
- }
- } catch (ex: RejectedExecutionException) {
- // Couldn't acquire a thread, cancel coroutine.
- continuation.cancel(
- IllegalStateException(
- "Unable to acquire a thread to perform the database transaction.", ex
- )
- )
- }
-}
-
-/**
- * Creates a [CoroutineContext] for performing database operations within a coroutine transaction.
- *
- * The context is a combination of a dispatcher, a [TransactionElement] and a thread local element.
- *
- * * The dispatcher will dispatch coroutines to a single thread that is taken over from the Room
- * transaction executor. If the coroutine context is switched, suspending DAO functions will be able
- * to dispatch to the transaction thread. In reality the dispatcher is the event loop of a
- * [runBlocking] started on the dedicated thread.
- *
- * * The [TransactionElement] serves as an indicator for inherited context, meaning, if there is a
- * switch of context, suspending DAO methods will be able to use the indicator to dispatch the
- * database operation to the transaction thread.
- *
- * * The thread local element serves as a second indicator and marks threads that are used to
- * execute coroutines within the coroutine transaction, more specifically it allows us to identify
- * if a blocking DAO method is invoked within the transaction coroutine. Never assign meaning to
- * this value, for now all we care is if its present or not.
- */
-private fun RoomDatabase.createTransactionContext(
- dispatcher: ContinuationInterceptor
-): CoroutineContext {
- val transactionElement = TransactionElement(dispatcher)
- val threadLocalElement =
- suspendingTransactionId.asContextElement(System.identityHashCode(transactionElement))
- return dispatcher + transactionElement + threadLocalElement
-}
-
-/**
- * A [CoroutineContext.Element] that indicates there is an on-going database transaction.
- *
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-internal class TransactionElement(
- internal val transactionDispatcher: ContinuationInterceptor
-) : CoroutineContext.Element {
-
- companion object Key : CoroutineContext.Key<TransactionElement>
-
- override val key: CoroutineContext.Key<TransactionElement>
- get() = TransactionElement
-
- /**
- * Number of transactions (including nested ones) started with this element.
- * Call [acquire] to increase the count and [release] to decrease it.
- */
- private val referenceCount = AtomicInteger(0)
-
- fun acquire() {
- referenceCount.incrementAndGet()
- }
-
- fun release() {
- val count = referenceCount.decrementAndGet()
- if (count < 0) {
- throw IllegalStateException("Transaction was never started or was already released.")
- }
- }
-}
-
-/**
- * Creates a [Flow] that listens for changes in the database via the [InvalidationTracker] and emits
- * sets of the tables that were invalidated.
- *
- * The Flow will emit at least one value, a set of all the tables registered for observation to
- * kick-start the stream unless [emitInitialState] is set to `false`.
- *
- * If one of the tables to observe does not exist in the database, this Flow throws an
- * [IllegalArgumentException] during collection.
- *
- * The returned Flow can be used to create a stream that reacts to changes in the database:
- * ```
- * fun getArtistTours(from: Date, to: Date): Flow<Map<Artist, TourState>> {
- * return db.invalidationTrackerFlow("Artist").map { _ ->
- * val artists = artistsDao.getAllArtists()
- * val tours = tourService.fetchStates(artists.map { it.id })
- * associateTours(artists, tours, from, to)
- * }
- * }
- * ```
- *
- * @param tables The name of the tables or views to observe.
- * @param emitInitialState Set to `false` if no initial emission is desired. Default value is
- * `true`.
- */
-public fun RoomDatabase.invalidationTrackerFlow(
- vararg tables: String,
- emitInitialState: Boolean = true
-): Flow<Set<String>> = callbackFlow {
- // Flag to ignore invalidation until the initial state is sent.
- val ignoreInvalidation = AtomicBoolean(emitInitialState)
- val observer = object : InvalidationTracker.Observer(tables) {
- override fun onInvalidated(tables: Set<String>) {
- if (ignoreInvalidation.get()) {
- return
- }
- trySend(tables)
- }
- }
- val queryContext =
- coroutineContext[TransactionElement]?.transactionDispatcher ?: getQueryDispatcher()
- val job = launch(queryContext) {
- invalidationTracker.addObserver(observer)
- try {
- if (emitInitialState) {
- // Initial invalidation of all tables, to kick-start the flow
- trySend(tables.toSet())
- }
- ignoreInvalidation.set(false)
- awaitCancellation()
- } finally {
- invalidationTracker.removeObserver(observer)
- }
- }
- awaitClose {
- job.cancel()
- }
-}
diff --git a/room/room-ktx/src/main/java/androidx/room/migration/MigrationExt.kt b/room/room-ktx/src/main/java/androidx/room/migration/MigrationExt.kt
deleted file mode 100644
index a697681..0000000
--- a/room/room-ktx/src/main/java/androidx/room/migration/MigrationExt.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-@file:JvmName("MigrationKt")
-
-package androidx.room.migration
-
-import androidx.sqlite.db.SupportSQLiteDatabase
-
-/**
- * Creates [Migration] from [startVersion] to [endVersion] that runs [migrate] to perform
- * the necessary migrations.
- *
- * A migration can handle more than 1 version (e.g. if you have a faster path to choose when
- * going version 3 to 5 without going to version 4). If Room opens a database at version
- * 3 and latest version is < 5, Room will use the migration object that can migrate from
- * 3 to 5 instead of 3 to 4 and 4 to 5.
- *
- * If there are not enough migrations provided to move from the current version to the latest
- * version, Room will clear the database and recreate so even if you have no changes between 2
- * versions, you should still provide a Migration object to the builder.
- *
- * [migrate] cannot access any generated Dao in this method.
- *
- * [migrate] is already called inside a transaction and that transaction
- * might actually be a composite transaction of all necessary `Migration`s.
- */
-public fun Migration(
- startVersion: Int,
- endVersion: Int,
- migrate: (SupportSQLiteDatabase) -> Unit
-): Migration = MigrationImpl(startVersion, endVersion, migrate)
-
-private class MigrationImpl(
- startVersion: Int,
- endVersion: Int,
- val migrateCallback: (SupportSQLiteDatabase) -> Unit
-) : Migration(startVersion, endVersion) {
- override fun migrate(db: SupportSQLiteDatabase) = migrateCallback(db)
-}
diff --git a/room/room-runtime/api/current.txt b/room/room-runtime/api/current.txt
index 57ca0f5..50d82d1 100644
--- a/room/room-runtime/api/current.txt
+++ b/room/room-runtime/api/current.txt
@@ -160,6 +160,11 @@
method public void onQuery(String sqlQuery, java.util.List<?> bindArgs);
}
+ public final class RoomDatabaseKt {
+ method public static kotlinx.coroutines.flow.Flow<java.util.Set<java.lang.String>> invalidationTrackerFlow(androidx.room.RoomDatabase, String[] tables, optional boolean emitInitialState);
+ method public static suspend <R> Object? withTransaction(androidx.room.RoomDatabase, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super R>);
+ }
+
public interface RoomOpenDelegateMarker {
}
@@ -180,5 +185,9 @@
field public final int startVersion;
}
+ public final class MigrationKt {
+ method public static androidx.room.migration.Migration Migration(int startVersion, int endVersion, kotlin.jvm.functions.Function1<? super androidx.sqlite.db.SupportSQLiteDatabase,kotlin.Unit> migrate);
+ }
+
}
diff --git a/room/room-runtime/api/restricted_current.txt b/room/room-runtime/api/restricted_current.txt
index 5b386e3..ceda800 100644
--- a/room/room-runtime/api/restricted_current.txt
+++ b/room/room-runtime/api/restricted_current.txt
@@ -1,6 +1,19 @@
// Signature format: 4.0
package androidx.room {
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class CoroutinesRoom {
+ method public static <R> kotlinx.coroutines.flow.Flow<R> createFlow(androidx.room.RoomDatabase db, boolean inTransaction, String[] tableNames, java.util.concurrent.Callable<R> callable);
+ method public static suspend <R> Object? execute(androidx.room.RoomDatabase db, boolean inTransaction, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Callable<R> callable, kotlin.coroutines.Continuation<? super R>);
+ method public static suspend <R> Object? execute(androidx.room.RoomDatabase db, boolean inTransaction, java.util.concurrent.Callable<R> callable, kotlin.coroutines.Continuation<? super R>);
+ field public static final androidx.room.CoroutinesRoom.Companion Companion;
+ }
+
+ public static final class CoroutinesRoom.Companion {
+ method public <R> kotlinx.coroutines.flow.Flow<R> createFlow(androidx.room.RoomDatabase db, boolean inTransaction, String[] tableNames, java.util.concurrent.Callable<R> callable);
+ method public suspend <R> Object? execute(androidx.room.RoomDatabase db, boolean inTransaction, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Callable<R> callable, kotlin.coroutines.Continuation<? super R>);
+ method public suspend <R> Object? execute(androidx.room.RoomDatabase db, boolean inTransaction, java.util.concurrent.Callable<R> callable, kotlin.coroutines.Continuation<? super R>);
+ }
+
public class DatabaseConfiguration {
ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public DatabaseConfiguration(android.content.Context context, String? name, androidx.sqlite.db.SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory, androidx.room.RoomDatabase.MigrationContainer migrationContainer, java.util.List<? extends androidx.room.RoomDatabase.Callback>? callbacks, boolean allowMainThreadQueries, androidx.room.RoomDatabase.JournalMode journalMode, java.util.concurrent.Executor queryExecutor, boolean requireMigration, java.util.Set<java.lang.Integer>? migrationNotRequiredFrom);
ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public DatabaseConfiguration(android.content.Context context, String? name, androidx.sqlite.db.SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory, androidx.room.RoomDatabase.MigrationContainer migrationContainer, java.util.List<? extends androidx.room.RoomDatabase.Callback>? callbacks, boolean allowMainThreadQueries, androidx.room.RoomDatabase.JournalMode journalMode, java.util.concurrent.Executor queryExecutor, java.util.concurrent.Executor transactionExecutor, android.content.Intent? multiInstanceInvalidationServiceIntent, boolean requireMigration, boolean allowDestructiveMigrationOnDowngrade, java.util.Set<java.lang.Integer>? migrationNotRequiredFrom, String? copyFromAssetPath, java.io.File? copyFromFile, java.util.concurrent.Callable<java.io.InputStream>? copyFromInputStream, androidx.room.RoomDatabase.PrepackagedDatabaseCallback? prepackagedDatabaseCallback, java.util.List<?> typeConverters, java.util.List<? extends androidx.room.migration.AutoMigrationSpec> autoMigrationSpecs);
@@ -218,6 +231,11 @@
method public void onQuery(String sqlQuery, java.util.List<?> bindArgs);
}
+ public final class RoomDatabaseKt {
+ method public static kotlinx.coroutines.flow.Flow<java.util.Set<java.lang.String>> invalidationTrackerFlow(androidx.room.RoomDatabase, String[] tables, optional boolean emitInitialState);
+ method public static suspend <R> Object? withTransaction(androidx.room.RoomDatabase, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super R>);
+ }
+
public interface RoomOpenDelegateMarker {
}
@@ -311,6 +329,10 @@
field public final int startVersion;
}
+ public final class MigrationKt {
+ method public static androidx.room.migration.Migration Migration(int startVersion, int endVersion, kotlin.jvm.functions.Function1<? super androidx.sqlite.db.SupportSQLiteDatabase,kotlin.Unit> migrate);
+ }
+
}
package androidx.room.paging {
diff --git a/room/room-runtime/build.gradle b/room/room-runtime/build.gradle
index fedf63e..9b92064 100644
--- a/room/room-runtime/build.gradle
+++ b/room/room-runtime/build.gradle
@@ -83,11 +83,13 @@
api(project(":room:room-common"))
api(project(":sqlite:sqlite"))
api(projectOrArtifact(":annotation:annotation"))
+ api(libs.kotlinCoroutinesCore)
}
}
commonTest {
dependencies {
implementation(libs.kotlinTest)
+ implementation(libs.kotlinCoroutinesTest)
implementation(project(":kruth:kruth"))
}
}
@@ -95,6 +97,7 @@
dependsOn(commonMain)
dependencies {
api(project(":sqlite:sqlite-framework"))
+ api(libs.kotlinCoroutinesAndroid)
implementation("androidx.arch.core:core-runtime:2.2.0")
compileOnly("androidx.collection:collection:1.2.0")
compileOnly("androidx.lifecycle:lifecycle-livedata-core:2.0.0")
diff --git a/room/room-ktx/src/androidTest/java/androidx/room/CoroutineRoomCancellationTest.kt b/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/CoroutineRoomCancellationTest.kt
similarity index 100%
rename from room/room-ktx/src/androidTest/java/androidx/room/CoroutineRoomCancellationTest.kt
rename to room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/CoroutineRoomCancellationTest.kt
diff --git a/room/room-ktx/src/main/java/androidx/room/CoroutinesRoom.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/CoroutinesRoom.android.kt
similarity index 100%
rename from room/room-ktx/src/main/java/androidx/room/CoroutinesRoom.kt
rename to room/room-runtime/src/androidMain/kotlin/androidx/room/CoroutinesRoom.android.kt
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt
index d178962..b94af2b 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt
@@ -13,6 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+@file:JvmName("RoomDatabaseKt")
+
package androidx.room
import android.annotation.SuppressLint
@@ -48,10 +50,27 @@
import java.util.TreeMap
import java.util.concurrent.Callable
import java.util.concurrent.Executor
+import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicBoolean
+import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.Lock
import java.util.concurrent.locks.ReentrantReadWriteLock
+import kotlin.coroutines.ContinuationInterceptor
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.coroutineContext
+import kotlin.coroutines.resume
import kotlin.reflect.KClass
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.asContextElement
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
/**
* Base class for all Room databases. All classes that are annotated with [Database] must
@@ -1729,3 +1748,204 @@
const val MAX_BIND_PARAMETER_CNT = 999
}
}
+
+/**
+ * Calls the specified suspending [block] in a database transaction. The transaction will be
+ * marked as successful unless an exception is thrown in the suspending [block] or the coroutine
+ * is cancelled.
+ *
+ * Room will only perform at most one transaction at a time, additional transactions are queued
+ * and executed on a first come, first serve order.
+ *
+ * Performing blocking database operations is not permitted in a coroutine scope other than the
+ * one received by the suspending block. It is recommended that all [Dao] function invoked within
+ * the [block] be suspending functions.
+ *
+ * The internal dispatcher used to execute the given [block] will block an utilize a thread from
+ * Room's transaction executor until the [block] is complete.
+ */
+public suspend fun <R> RoomDatabase.withTransaction(block: suspend () -> R): R {
+ val transactionBlock: suspend CoroutineScope.() -> R = transaction@{
+ val transactionElement = coroutineContext[TransactionElement]!!
+ transactionElement.acquire()
+ try {
+ @Suppress("DEPRECATION")
+ beginTransaction()
+ try {
+ val result = block.invoke()
+ @Suppress("DEPRECATION")
+ setTransactionSuccessful()
+ return@transaction result
+ } finally {
+ @Suppress("DEPRECATION")
+ endTransaction()
+ }
+ } finally {
+ transactionElement.release()
+ }
+ }
+ // Use inherited transaction context if available, this allows nested suspending transactions.
+ val transactionDispatcher = coroutineContext[TransactionElement]?.transactionDispatcher
+ return if (transactionDispatcher != null) {
+ withContext(transactionDispatcher, transactionBlock)
+ } else {
+ startTransactionCoroutine(coroutineContext, transactionBlock)
+ }
+}
+
+/**
+ * Suspend caller coroutine and start the transaction coroutine in a thread from the
+ * [RoomDatabase.transactionExecutor], resuming the caller coroutine with the result once done.
+ * The [context] will be a parent of the started coroutine to propagating cancellation and release
+ * the thread when cancelled.
+ */
+private suspend fun <R> RoomDatabase.startTransactionCoroutine(
+ context: CoroutineContext,
+ transactionBlock: suspend CoroutineScope.() -> R
+): R = suspendCancellableCoroutine { continuation ->
+ try {
+ transactionExecutor.execute {
+ try {
+ // Thread acquired, start the transaction coroutine using the parent context.
+ // The started coroutine will have an event loop dispatcher that we'll use for the
+ // transaction context.
+ runBlocking(context.minusKey(ContinuationInterceptor)) {
+ val dispatcher = coroutineContext[ContinuationInterceptor]!!
+ val transactionContext = createTransactionContext(dispatcher)
+ continuation.resume(
+ withContext(transactionContext, transactionBlock)
+ )
+ }
+ } catch (ex: Throwable) {
+ // If anything goes wrong, propagate exception to the calling coroutine.
+ continuation.cancel(ex)
+ }
+ }
+ } catch (ex: RejectedExecutionException) {
+ // Couldn't acquire a thread, cancel coroutine.
+ continuation.cancel(
+ IllegalStateException(
+ "Unable to acquire a thread to perform the database transaction.", ex
+ )
+ )
+ }
+}
+
+/**
+ * Creates a [CoroutineContext] for performing database operations within a coroutine transaction.
+ *
+ * The context is a combination of a dispatcher, a [TransactionElement] and a thread local element.
+ *
+ * * The dispatcher will dispatch coroutines to a single thread that is taken over from the Room
+ * transaction executor. If the coroutine context is switched, suspending DAO functions will be able
+ * to dispatch to the transaction thread. In reality the dispatcher is the event loop of a
+ * [runBlocking] started on the dedicated thread.
+ *
+ * * The [TransactionElement] serves as an indicator for inherited context, meaning, if there is a
+ * switch of context, suspending DAO methods will be able to use the indicator to dispatch the
+ * database operation to the transaction thread.
+ *
+ * * The thread local element serves as a second indicator and marks threads that are used to
+ * execute coroutines within the coroutine transaction, more specifically it allows us to identify
+ * if a blocking DAO method is invoked within the transaction coroutine. Never assign meaning to
+ * this value, for now all we care is if its present or not.
+ */
+private fun RoomDatabase.createTransactionContext(
+ dispatcher: ContinuationInterceptor
+): CoroutineContext {
+ val transactionElement = TransactionElement(dispatcher)
+ val threadLocalElement =
+ suspendingTransactionId.asContextElement(System.identityHashCode(transactionElement))
+ return dispatcher + transactionElement + threadLocalElement
+}
+
+/**
+ * A [CoroutineContext.Element] that indicates there is an on-going database transaction.
+ *
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+internal class TransactionElement(
+ internal val transactionDispatcher: ContinuationInterceptor
+) : CoroutineContext.Element {
+
+ companion object Key : CoroutineContext.Key<TransactionElement>
+
+ override val key: CoroutineContext.Key<TransactionElement>
+ get() = TransactionElement
+
+ /**
+ * Number of transactions (including nested ones) started with this element.
+ * Call [acquire] to increase the count and [release] to decrease it.
+ */
+ private val referenceCount = AtomicInteger(0)
+
+ fun acquire() {
+ referenceCount.incrementAndGet()
+ }
+
+ fun release() {
+ val count = referenceCount.decrementAndGet()
+ if (count < 0) {
+ throw IllegalStateException("Transaction was never started or was already released.")
+ }
+ }
+}
+
+/**
+ * Creates a [Flow] that listens for changes in the database via the [InvalidationTracker] and emits
+ * sets of the tables that were invalidated.
+ *
+ * The Flow will emit at least one value, a set of all the tables registered for observation to
+ * kick-start the stream unless [emitInitialState] is set to `false`.
+ *
+ * If one of the tables to observe does not exist in the database, this Flow throws an
+ * [IllegalArgumentException] during collection.
+ *
+ * The returned Flow can be used to create a stream that reacts to changes in the database:
+ * ```
+ * fun getArtistTours(from: Date, to: Date): Flow<Map<Artist, TourState>> {
+ * return db.invalidationTrackerFlow("Artist").map { _ ->
+ * val artists = artistsDao.getAllArtists()
+ * val tours = tourService.fetchStates(artists.map { it.id })
+ * associateTours(artists, tours, from, to)
+ * }
+ * }
+ * ```
+ *
+ * @param tables The name of the tables or views to observe.
+ * @param emitInitialState Set to `false` if no initial emission is desired. Default value is
+ * `true`.
+ */
+public fun RoomDatabase.invalidationTrackerFlow(
+ vararg tables: String,
+ emitInitialState: Boolean = true
+): Flow<Set<String>> = callbackFlow {
+ // Flag to ignore invalidation until the initial state is sent.
+ val ignoreInvalidation = AtomicBoolean(emitInitialState)
+ val observer = object : InvalidationTracker.Observer(tables) {
+ override fun onInvalidated(tables: Set<String>) {
+ if (ignoreInvalidation.get()) {
+ return
+ }
+ trySend(tables)
+ }
+ }
+ val queryContext =
+ coroutineContext[TransactionElement]?.transactionDispatcher ?: getQueryDispatcher()
+ val job = launch(queryContext) {
+ invalidationTracker.addObserver(observer)
+ try {
+ if (emitInitialState) {
+ // Initial invalidation of all tables, to kick-start the flow
+ trySend(tables.toSet())
+ }
+ ignoreInvalidation.set(false)
+ awaitCancellation()
+ } finally {
+ invalidationTracker.removeObserver(observer)
+ }
+ }
+ awaitClose {
+ job.cancel()
+ }
+}
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/migration/Migration.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/migration/Migration.android.kt
index 07838889..dbad445 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/migration/Migration.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/migration/Migration.android.kt
@@ -13,6 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+@file:JvmName("MigrationKt")
+
package androidx.room.migration
import androidx.room.driver.SupportSQLiteConnection
@@ -70,3 +72,35 @@
}
}
}
+
+/**
+ * Creates [Migration] from [startVersion] to [endVersion] that runs [migrate] to perform
+ * the necessary migrations.
+ *
+ * A migration can handle more than 1 version (e.g. if you have a faster path to choose when
+ * going version 3 to 5 without going to version 4). If Room opens a database at version
+ * 3 and latest version is < 5, Room will use the migration object that can migrate from
+ * 3 to 5 instead of 3 to 4 and 4 to 5.
+ *
+ * If there are not enough migrations provided to move from the current version to the latest
+ * version, Room will clear the database and recreate so even if you have no changes between 2
+ * versions, you should still provide a Migration object to the builder.
+ *
+ * [migrate] cannot access any generated Dao in this method.
+ *
+ * [migrate] is already called inside a transaction and that transaction
+ * might actually be a composite transaction of all necessary `Migration`s.
+ */
+public fun Migration(
+ startVersion: Int,
+ endVersion: Int,
+ migrate: (SupportSQLiteDatabase) -> Unit
+): Migration = MigrationImpl(startVersion, endVersion, migrate)
+
+private class MigrationImpl(
+ startVersion: Int,
+ endVersion: Int,
+ val migrateCallback: (SupportSQLiteDatabase) -> Unit
+) : Migration(startVersion, endVersion) {
+ override fun migrate(db: SupportSQLiteDatabase) = migrateCallback(db)
+}
diff --git a/room/room-ktx/src/test/java/androidx/room/CoroutinesRoomTest.kt b/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/CoroutinesRoomTest.kt
similarity index 100%
rename from room/room-ktx/src/test/java/androidx/room/CoroutinesRoomTest.kt
rename to room/room-runtime/src/androidUnitTest/kotlin/androidx/room/CoroutinesRoomTest.kt
diff --git a/room/room-ktx/src/test/java/androidx/room/MigrationTest.kt b/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/MigrationTest.kt
similarity index 100%
rename from room/room-ktx/src/test/java/androidx/room/MigrationTest.kt
rename to room/room-runtime/src/androidUnitTest/kotlin/androidx/room/MigrationTest.kt
diff --git a/work/integration-tests/testapp/build.gradle b/work/integration-tests/testapp/build.gradle
index 1925b84..7bac479 100644
--- a/work/integration-tests/testapp/build.gradle
+++ b/work/integration-tests/testapp/build.gradle
@@ -45,6 +45,7 @@
dependencies {
annotationProcessor(projectOrArtifact(":room:room-compiler"))
implementation(projectOrArtifact(":room:room-runtime"))
+ implementation(projectOrArtifact(":room:room-ktx"))
implementation(libs.constraintLayout)
implementation("androidx.core:core:1.12.0")
diff --git a/work/work-benchmark/build.gradle b/work/work-benchmark/build.gradle
index cd0c8a6..29676c5 100644
--- a/work/work-benchmark/build.gradle
+++ b/work/work-benchmark/build.gradle
@@ -28,6 +28,7 @@
androidTestImplementation(project(":work:work-multiprocess"))
androidTestImplementation(projectOrArtifact(":benchmark:benchmark-junit4"))
androidTestImplementation(projectOrArtifact(":room:room-runtime"))
+ androidTestImplementation(projectOrArtifact(":room:room-ktx"))
androidTestImplementation(libs.junit)
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)