diff --git a/app/build.gradle b/app/build.gradle index 2ff9896076..5e66458812 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -495,7 +495,7 @@ configurations.configureEach { } else if (details.requested.group == "androidx.lifecycle" && details.requested.name != "lifecycle-extensions") { //print("Pinning " + details.requested.group + ":" + details.requested.name + "\n") - details.useVersion "2.6.2" + details.useVersion "2.7.0" } else if (details.requested.group == "org.apache.poi") { //print("Pinning " + details.requested.group + ":" + details.requested.name + "\n") details.useVersion "3.17" @@ -514,15 +514,15 @@ dependencies { def desugar_version = "2.0.4" def startup_version = "1.1.1" // 1.2.0-alpha02 - def annotation_version_experimental = "1.3.1" // 1.4.0-beta01 + def annotation_version_experimental = "1.3.1" // 1.4.0-rc01 def core_version = "1.12.0" // 1.13.0-alpha02 def appcompat_version = "1.6.1" // 1.7.0-alpha03 def emoji_version = "1.4.0" // 1.5.0-alpha01 def flatbuffers_version = "2.0.0" def activity_version = "1.8.2" // 1.9.0-alpha01 - def fragment_version = "1.6.2" // 1.7.0-alpha07 + def fragment_version = "1.6.2" // 1.7.0-alpha08 def windows_version = "1.2.0" // 1.3.0-alpha01 - def webkit_version = "1.9.0" // 1.10.0-beta01 + def webkit_version = "1.9.0" // 1.10.0-rc01 def recyclerview_version = "1.3.2" // 1.4.0-alpha01 def coordinatorlayout_version = "1.2.0" // 1.3.0-alpha02 def constraintlayout_version = "2.1.4" // 2.2.0-alpha13 @@ -531,7 +531,7 @@ dependencies { def lbm_version = "1.1.0" def swiperefresh_version = "1.2.0-alpha01" def documentfile_version = "1.1.0-alpha01" - def lifecycle_version = "2.6.2" // 2.7.0-rc02 + def lifecycle_version = "2.7.0" def lifecycle_extensions_version = "2.2.0" def room_version = "2.4.3" // 2.5.2/2.6.1 def sqlite_version = "2.4.0" diff --git a/app/src/main/java/androidx/lifecycle/CoroutineLiveData.kt b/app/src/main/java/androidx/lifecycle/CoroutineLiveData.kt new file mode 100644 index 0000000000..1cdc3036d8 --- /dev/null +++ b/app/src/main/java/androidx/lifecycle/CoroutineLiveData.kt @@ -0,0 +1,478 @@ +/* + * 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. + */ + +package androidx.lifecycle + +import android.annotation.SuppressLint +import android.os.Build +import androidx.annotation.MainThread +import androidx.annotation.RequiresApi +import java.time.Duration +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +internal const val DEFAULT_TIMEOUT = 5000L + +/** + * Interface that allows controlling a [LiveData] from a coroutine block. + * + * @see liveData + */ +public interface LiveDataScope { + /** + * Set's the [LiveData]'s value to the given [value]. If you've called [emitSource] previously, + * calling [emit] will remove that source. + * + * Note that this function suspends until the value is set on the [LiveData]. + * + * @param value The new value for the [LiveData] + * + * @see emitSource + */ + public suspend fun emit(value: T) + + /** + * Add the given [LiveData] as a source, similar to [MediatorLiveData.addSource]. Calling this + * method will remove any source that was yielded before via [emitSource]. + * + * @param source The [LiveData] instance whose values will be dispatched from the current + * [LiveData]. + * + * @see emit + * @see MediatorLiveData.addSource + * @see MediatorLiveData.removeSource + */ + public suspend fun emitSource(source: LiveData): DisposableHandle + + /** + * References the current value of the [LiveData]. + * + * If the block never `emit`ed a value, [latestValue] will be `null`. You can use this + * value to check what was then latest value `emit`ed by your `block` before it got cancelled. + * + * Note that if the block called [emitSource], then `latestValue` will be last value + * dispatched by the `source` [LiveData]. + */ + public val latestValue: T? +} + +internal class LiveDataScopeImpl( + internal var target: CoroutineLiveData, + context: CoroutineContext +) : LiveDataScope { + + override val latestValue: T? + get() = target.value + + // use `liveData` provided context + main dispatcher to communicate with the target + // LiveData. This gives us main thread safety as well as cancellation cooperation + private val coroutineContext = context + Dispatchers.Main.immediate + + override suspend fun emitSource(source: LiveData): DisposableHandle = + withContext(coroutineContext) { + return@withContext target.emitSource(source) + } + + @SuppressLint("NullSafeMutableLiveData") + override suspend fun emit(value: T) = withContext(coroutineContext) { + target.clearSource() + target.value = value + } +} + +internal suspend fun MediatorLiveData.addDisposableSource( + source: LiveData +): EmittedSource = withContext(Dispatchers.Main.immediate) { + addSource(source) { + value = it + } + EmittedSource( + source = source, + mediator = this@addDisposableSource + ) +} + +/** + * Holder class that keeps track of the previously dispatched [LiveData]. + * It implements [DisposableHandle] interface while also providing a suspend clear function + * that we can use internally. + */ +internal class EmittedSource( + private val source: LiveData<*>, + private val mediator: MediatorLiveData<*> +) : DisposableHandle { + // @MainThread + private var disposed = false + /** + * Unlike [dispose] which cannot be sync because it not a coroutine (and we do not want to + * lock), this version is a suspend function and does not return until source is removed. + */ + suspend fun disposeNow() = withContext(Dispatchers.Main.immediate) { + removeSource() + } + + override fun dispose() { + CoroutineScope(Dispatchers.Main.immediate).launch { + removeSource() + } + } + + @MainThread + private fun removeSource() { + if (!disposed) { + mediator.removeSource(source) + disposed = true + } + } +} + +internal typealias Block = suspend LiveDataScope.() -> Unit + +/** + * Handles running a block at most once to completion. + */ +internal class BlockRunner( + private val liveData: CoroutineLiveData, + private val block: Block, + private val timeoutInMs: Long, + private val scope: CoroutineScope, + private val onDone: () -> Unit +) { + // currently running block job. + private var runningJob: Job? = null + + // cancelation job created in cancel. + private var cancellationJob: Job? = null + + @MainThread + fun maybeRun() { + cancellationJob?.cancel() + cancellationJob = null + if (runningJob != null) { + return + } + runningJob = scope.launch { + val liveDataScope = LiveDataScopeImpl(liveData, coroutineContext) + block(liveDataScope) + onDone() + } + } + + @MainThread + fun cancel() { + if (cancellationJob != null) { + error("Cancel call cannot happen without a maybeRun") + } + cancellationJob = scope.launch(Dispatchers.Main.immediate) { + delay(timeoutInMs) + if (!liveData.hasActiveObservers()) { + // one last check on active observers to avoid any race condition between starting + // a running coroutine and cancelation + runningJob?.cancel() + runningJob = null + } + } + } +} + +internal class CoroutineLiveData( + context: CoroutineContext = EmptyCoroutineContext, + timeoutInMs: Long = DEFAULT_TIMEOUT, + block: Block +) : MediatorLiveData() { + private var blockRunner: BlockRunner? + private var emittedSource: EmittedSource? = null + + init { + // use an intermediate supervisor job so that if we cancel individual block runs due to losing + // observers, it won't cancel the given context as we only cancel w/ the intention of possibly + // relaunching using the same parent context. + val supervisorJob = SupervisorJob(context[Job]) + + // The scope for this LiveData where we launch every block Job. + // We default to Main dispatcher but developer can override it. + // The supervisor job is added last to isolate block runs. + val scope = CoroutineScope(Dispatchers.Main.immediate + context + supervisorJob) + blockRunner = BlockRunner( + liveData = this, + block = block, + timeoutInMs = timeoutInMs, + scope = scope + ) { + blockRunner = null + } + } + + internal suspend fun emitSource(source: LiveData): DisposableHandle { + clearSource() + val newSource = addDisposableSource(source) + emittedSource = newSource + return newSource + } + + internal suspend fun clearSource() { + emittedSource?.disposeNow() + emittedSource = null + } + + override fun onActive() { + super.onActive() + blockRunner?.maybeRun() + } + + override fun onInactive() { + super.onInactive() + blockRunner?.cancel() + } +} + +/** + * Builds a LiveData that has values yielded from the given [block] that executes on a + * [LiveDataScope]. + * + * The [block] starts executing when the returned [LiveData] becomes [active](LiveData.onActive). + * If the [LiveData] becomes [inactive](LiveData.onInactive) while the [block] is executing, it + * will be cancelled after [timeoutInMs] milliseconds unless the [LiveData] becomes active again + * before that timeout (to gracefully handle cases like Activity rotation). Any value + * [LiveDataScope.emit]ed from a cancelled [block] will be ignored. + * + * After a cancellation, if the [LiveData] becomes active again, the [block] will be re-executed + * from the beginning. If you would like to continue the operations based on where it was stopped + * last, you can use the [LiveDataScope.latestValue] function to get the last + * [LiveDataScope.emit]ed value. + + * If the [block] completes successfully *or* is cancelled due to reasons other than [LiveData] + * becoming inactive, it *will not* be re-executed even after [LiveData] goes through active + * inactive cycle. + * + * As a best practice, it is important for the [block] to cooperate in cancellation. See kotlin + * coroutines documentation for details + * https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html. + * + * The [timeoutInMs] can be changed to fit different use cases better, for example increasing it + * will give more time to the [block] to complete even if [LiveData] is inactive. It is good for + * cases when [block] is finite (meaning it can complete successfully) and is costly to restart. + * Otherwise if a [block] is cheap to restart, decreasing the [timeoutInMs] value will allow to + * yield less values that aren't consumed by anything. + * + * ``` + * // a simple LiveData that receives value 3, 3 seconds after being observed for the first time. + * val data : LiveData = liveData { + * delay(3000) + * emit(3) + * } + * + * + * // a LiveData that fetches a `User` object based on a `userId` and refreshes it every 30 seconds + * // as long as it is observed + * val userId : LiveData = ... + * val user = userId.switchMap { id -> + * liveData { + * while(true) { + * // note that `while(true)` is fine because the `delay(30_000)` below will cooperate in + * // cancellation if LiveData is not actively observed anymore + * val data = api.fetch(id) // errors are ignored for brevity + * emit(data) + * delay(30_000) + * } + * } + * } + * + * // A retrying data fetcher with doubling back-off + * val user = liveData { + * var backOffTime = 1_000 + * var succeeded = false + * while(!succeeded) { + * try { + * emit(api.fetch(id)) + * succeeded = true + * } catch(ioError : IOException) { + * delay(backOffTime) + * backOffTime *= minOf(backOffTime * 2, 60_000) + * } + * } + * } + * + * // a LiveData that tries to load the `User` from local cache first and then tries to fetch + * // from the server and also yields the updated value + * val user = liveData { + * // dispatch loading first + * emit(LOADING(id)) + * // check local storage + * val cached = cache.loadUser(id) + * if (cached != null) { + * emit(cached) + * } + * if (cached == null || cached.isStale()) { + * val fresh = api.fetch(id) // errors are ignored for brevity + * cache.save(fresh) + * emit(fresh) + * } + * } + * + * // a LiveData that immediately receives a LiveData from the database and yields it as a + * // source but also tries to back-fill the database from the server + * val user = liveData { + * val fromDb: LiveData = roomDatabase.loadUser(id) + * emitSource(fromDb) + * val updated = api.fetch(id) // errors are ignored for brevity + * // Since we are using Room here, updating the database will update the `fromDb` LiveData + * // that was obtained above. See Room's documentation for more details. + * // https://developer.android.com/training/data-storage/room/accessing-data#query-observable + * roomDatabase.insert(updated) + * } + * ``` + * + * @param context The CoroutineContext to run the given block in. Defaults to + * [EmptyCoroutineContext] combined with + * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate] + * @param timeoutInMs The timeout in ms before cancelling the block if there are no active observers + * ([LiveData.hasActiveObservers]. Defaults to [DEFAULT_TIMEOUT]. + * @param block The block to run when the [LiveData] has active observers. + */ +@JvmOverloads +public fun liveData( + context: CoroutineContext = EmptyCoroutineContext, + timeoutInMs: Long = DEFAULT_TIMEOUT, + block: suspend LiveDataScope.() -> Unit +): LiveData = CoroutineLiveData(context, timeoutInMs, block) + +/** + * Builds a LiveData that has values yielded from the given [block] that executes on a + * [LiveDataScope]. + * + * The [block] starts executing when the returned [LiveData] becomes [active](LiveData.onActive). + * If the [LiveData] becomes [inactive](LiveData.onInactive) while the [block] is executing, it + * will be cancelled after the [timeout] duration unless the [LiveData] becomes active again + * before that timeout (to gracefully handle cases like Activity rotation). Any value + * [LiveDataScope.emit]ed from a cancelled [block] will be ignored. + * + * After a cancellation, if the [LiveData] becomes active again, the [block] will be re-executed + * from the beginning. If you would like to continue the operations based on where it was stopped + * last, you can use the [LiveDataScope.latestValue] function to get the last + * [LiveDataScope.emit]ed value. + + * If the [block] completes successfully *or* is cancelled due to reasons other than [LiveData] + * becoming inactive, it *will not* be re-executed even after [LiveData] goes through active + * inactive cycle. + * + * As a best practice, it is important for the [block] to cooperate in cancellation. See kotlin + * coroutines documentation for details + * https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html. + * + * The [timeout] can be changed to fit different use cases better, for example increasing it + * will give more time to the [block] to complete even if [LiveData] is inactive. It is good for + * cases when [block] is finite (meaning it can complete successfully) and is costly to restart. + * Otherwise if a [block] is cheap to restart, decreasing the [timeout] value will allow to + * yield less values that aren't consumed by anything. + * + * ``` + * // a simple LiveData that receives value 3, 3 seconds after being observed for the first time. + * val data : LiveData = liveData { + * delay(3000) + * emit(3) + * } + * + * + * // a LiveData that fetches a `User` object based on a `userId` and refreshes it every 30 seconds + * // as long as it is observed + * val userId : LiveData = ... + * val user = userId.switchMap { id -> + * liveData { + * while(true) { + * // note that `while(true)` is fine because the `delay(30_000)` below will cooperate in + * // cancellation if LiveData is not actively observed anymore + * val data = api.fetch(id) // errors are ignored for brevity + * emit(data) + * delay(30_000) + * } + * } + * } + * + * // A retrying data fetcher with doubling back-off + * val user = liveData { + * var backOffTime = 1_000 + * var succeeded = false + * while(!succeeded) { + * try { + * emit(api.fetch(id)) + * succeeded = true + * } catch(ioError : IOException) { + * delay(backOffTime) + * backOffTime *= minOf(backOffTime * 2, 60_000) + * } + * } + * } + * + * // a LiveData that tries to load the `User` from local cache first and then tries to fetch + * // from the server and also yields the updated value + * val user = liveData { + * // dispatch loading first + * emit(LOADING(id)) + * // check local storage + * val cached = cache.loadUser(id) + * if (cached != null) { + * emit(cached) + * } + * if (cached == null || cached.isStale()) { + * val fresh = api.fetch(id) // errors are ignored for brevity + * cache.save(fresh) + * emit(fresh) + * } + * } + * + * // a LiveData that immediately receives a LiveData from the database and yields it as a + * // source but also tries to back-fill the database from the server + * val user = liveData { + * val fromDb: LiveData = roomDatabase.loadUser(id) + * emitSource(fromDb) + * val updated = api.fetch(id) // errors are ignored for brevity + * // Since we are using Room here, updating the database will update the `fromDb` LiveData + * // that was obtained above. See Room's documentation for more details. + * // https://developer.android.com/training/data-storage/room/accessing-data#query-observable + * roomDatabase.insert(updated) + * } + * ``` + * + * @param context The CoroutineContext to run the given block in. Defaults to + * [EmptyCoroutineContext] combined with + * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate]. + * @param timeout The timeout duration before cancelling the block if there are no active observers + * ([LiveData.hasActiveObservers]. + * @param block The block to run when the [LiveData] has active observers. + */ +@RequiresApi(Build.VERSION_CODES.O) +@JvmOverloads +public fun liveData( + timeout: Duration, + context: CoroutineContext = EmptyCoroutineContext, + block: suspend LiveDataScope.() -> Unit +): LiveData = CoroutineLiveData(context, Api26Impl.toMillis(timeout), block) + +@RequiresApi(26) +internal object Api26Impl { + fun toMillis(timeout: Duration): Long { + return timeout.toMillis() + } +} diff --git a/app/src/main/java/androidx/lifecycle/FlowLiveData.kt b/app/src/main/java/androidx/lifecycle/FlowLiveData.kt new file mode 100644 index 0000000000..72d3ef74ee --- /dev/null +++ b/app/src/main/java/androidx/lifecycle/FlowLiveData.kt @@ -0,0 +1,161 @@ +/* + * 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("FlowLiveDataConversions") + +package androidx.lifecycle + +import android.annotation.SuppressLint +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.arch.core.executor.ArchTaskExecutor +import java.time.Duration +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * Creates a LiveData that has values collected from the origin [Flow]. + * + * If the origin [Flow] is a [StateFlow], then the initial value will be populated + * to the [LiveData]'s value field on the main thread. + * + * The upstream flow collection starts when the returned [LiveData] becomes active + * ([LiveData.onActive]). + * If the [LiveData] becomes inactive ([LiveData.onInactive]) while the flow has not completed, + * the flow collection will be cancelled after [timeoutInMs] milliseconds unless the [LiveData] + * becomes active again before that timeout (to gracefully handle cases like Activity rotation). + * + * After a cancellation, if the [LiveData] becomes active again, the upstream flow collection will + * be re-executed. + * + * If the upstream flow completes successfully *or* is cancelled due to reasons other than + * [LiveData] becoming inactive, it *will not* be re-collected even after [LiveData] goes through + * active inactive cycle. + * + * If flow completes with an exception, then exception will be delivered to the + * [CoroutineExceptionHandler][kotlinx.coroutines.CoroutineExceptionHandler] of provided [context]. + * By default [EmptyCoroutineContext] is used to so an exception will be delivered to main's + * thread [UncaughtExceptionHandler][Thread.UncaughtExceptionHandler]. If your flow upstream is + * expected to throw, you can use [catch operator][kotlinx.coroutines.flow.catch] on upstream flow + * to emit a helpful error object. + * + * The [timeoutInMs] can be changed to fit different use cases better, for example increasing it + * will give more time to flow to complete before being canceled and is good for finite flows + * that are costly to restart. Otherwise if a flow is cheap to restart decreasing the [timeoutInMs] + * value will allow to produce less values that aren't consumed by anything. + * + * @param context The CoroutineContext to collect the upstream flow in. Defaults to + * [EmptyCoroutineContext] combined with + * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate] + * @param timeoutInMs The timeout in ms before cancelling the block if there are no active observers + * ([LiveData.hasActiveObservers]. Defaults to [DEFAULT_TIMEOUT]. + */ +@JvmOverloads +public fun Flow.asLiveData( + context: CoroutineContext = EmptyCoroutineContext, + timeoutInMs: Long = DEFAULT_TIMEOUT +): LiveData = liveData(context, timeoutInMs) { + collect { + emit(it) + } +}.also { liveData -> + val flow = this + if (flow is StateFlow) { + @SuppressLint("RestrictedApi") + if (ArchTaskExecutor.getInstance().isMainThread) { + liveData.value = flow.value + } else { + liveData.postValue(flow.value) + } + } +} + +/** + * Creates a [Flow] containing values dispatched by originating [LiveData]: at the start + * a flow collector receives the latest value held by LiveData and then observes LiveData updates. + * + * When a collection of the returned flow starts the originating [LiveData] becomes + * [active][LiveData.onActive]. Similarly, when a collection completes [LiveData] becomes + * [inactive][LiveData.onInactive]. + * + * BackPressure: the returned flow is conflated. There is no mechanism to suspend an emission by + * LiveData due to a slow collector, so collector always gets the most recent value emitted. + */ +@OptIn(DelicateCoroutinesApi::class) +public fun LiveData.asFlow(): Flow = callbackFlow { + val observer = Observer { + trySend(it) + } + withContext(Dispatchers.Main.immediate) { + observeForever(observer) + } + + awaitClose { + GlobalScope.launch(Dispatchers.Main.immediate) { + removeObserver(observer) + } + } +}.conflate() + +/** + * Creates a LiveData that has values collected from the origin [Flow]. + * + * The upstream flow collection starts when the returned [LiveData] becomes active + * ([LiveData.onActive]). + * If the [LiveData] becomes inactive ([LiveData.onInactive]) while the flow has not completed, + * the flow collection will be cancelled after [timeout] unless the [LiveData] + * becomes active again before that timeout (to gracefully handle cases like Activity rotation). + * + * After a cancellation, if the [LiveData] becomes active again, the upstream flow collection will + * be re-executed. + * + * If the upstream flow completes successfully *or* is cancelled due to reasons other than + * [LiveData] becoming inactive, it *will not* be re-collected even after [LiveData] goes through + * active inactive cycle. + * + * If flow completes with an exception, then exception will be delivered to the + * [CoroutineExceptionHandler][kotlinx.coroutines.CoroutineExceptionHandler] of provided [context]. + * By default [EmptyCoroutineContext] is used to so an exception will be delivered to main's + * thread [UncaughtExceptionHandler][Thread.UncaughtExceptionHandler]. If your flow upstream is + * expected to throw, you can use [catch operator][kotlinx.coroutines.flow.catch] on upstream flow + * to emit a helpful error object. + * + * The [timeout] can be changed to fit different use cases better, for example increasing it + * will give more time to flow to complete before being canceled and is good for finite flows + * that are costly to restart. Otherwise if a flow is cheap to restart decreasing the [timeout] + * value will allow to produce less values that aren't consumed by anything. + * + * @param context The CoroutineContext to collect the upstream flow in. Defaults to + * [EmptyCoroutineContext] combined with + * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate] + * @param timeout The timeout in ms before cancelling the block if there are no active observers + * ([LiveData.hasActiveObservers]. Defaults to [DEFAULT_TIMEOUT]. + */ +@RequiresApi(Build.VERSION_CODES.O) +public fun Flow.asLiveData( + timeout: Duration, + context: CoroutineContext = EmptyCoroutineContext +): LiveData = asLiveData(context, Api26Impl.toMillis(timeout)) diff --git a/app/src/main/java/androidx/lifecycle/MediatorLiveData.java b/app/src/main/java/androidx/lifecycle/MediatorLiveData.java index 1fe6ced11c..0b352e958f 100644 --- a/app/src/main/java/androidx/lifecycle/MediatorLiveData.java +++ b/app/src/main/java/androidx/lifecycle/MediatorLiveData.java @@ -32,10 +32,10 @@ import java.util.Map; * objects. *

* Consider the following scenario: we have 2 instances of {@code LiveData}, let's name them - * {@code liveData1} and {@code liveData2}, and we want to merge their emissions in one object: - * {@code liveDataMerger}. Then, {@code liveData1} and {@code liveData2} will become sources for - * the {@code MediatorLiveData liveDataMerger} and every time {@code onChanged} callback - * is called for either of them, we set a new value in {@code liveDataMerger}. + * {@code liveData1} and {@code liveData2}, and we want to merge their emissions in one + * {@link MediatorLiveData} object: {@code liveDataMerger}. Then, {@code liveData1} and {@code + * liveData2} will become sources for the {@code liveDataMerger} and every time {@code onChanged} + * callback is called for either of them, we set a new value in {@code liveDataMerger}. * *

  * LiveData<Integer> liveData1 = ...;
diff --git a/app/src/main/java/androidx/lifecycle/Observer.kt b/app/src/main/java/androidx/lifecycle/Observer.kt
index 2df6295d87..3ce480f14d 100644
--- a/app/src/main/java/androidx/lifecycle/Observer.kt
+++ b/app/src/main/java/androidx/lifecycle/Observer.kt
@@ -23,7 +23,7 @@ package androidx.lifecycle
 fun interface Observer {
 
     /**
-     * Called when the data is changed is changed to [value].
+     * Called when the data is changed to [value].
      */
     fun onChanged(value: T)
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/androidx/lifecycle/Transformations.kt b/app/src/main/java/androidx/lifecycle/Transformations.kt
index c3b9ed6894..e7bbf1f23d 100644
--- a/app/src/main/java/androidx/lifecycle/Transformations.kt
+++ b/app/src/main/java/androidx/lifecycle/Transformations.kt
@@ -45,10 +45,14 @@ import androidx.arch.core.util.Function
 @JvmName("map")
 @MainThread
 @CheckResult
+@Suppress("UNCHECKED_CAST")
 fun  LiveData.map(
     transform: (@JvmSuppressWildcards X) -> (@JvmSuppressWildcards Y)
 ): LiveData {
     val result = MediatorLiveData()
+    if (isInitialized) {
+        result.value = transform(value as X)
+    }
     result.addSource(this) { x -> result.value = transform(x) }
     return result
 }
@@ -113,18 +117,21 @@ fun  LiveData.map(mapFunction: Function): LiveData {
 @JvmName("switchMap")
 @MainThread
 @CheckResult
+@Suppress("UNCHECKED_CAST")
 fun  LiveData.switchMap(
     transform: (@JvmSuppressWildcards X) -> (@JvmSuppressWildcards LiveData)?
 ): LiveData {
     val result = MediatorLiveData()
-    result.addSource(this, object : Observer {
-        var liveData: LiveData? = null
-
-        override fun onChanged(value: X) {
-            val newLiveData = transform(value)
-            if (liveData === newLiveData) {
-                return
-            }
+    var liveData: LiveData? = null
+    if (isInitialized) {
+        val initialLiveData = transform(value as X)
+        if (initialLiveData != null && initialLiveData.isInitialized) {
+            result.value = initialLiveData.value
+        }
+    }
+    result.addSource(this) { value: X ->
+        val newLiveData = transform(value)
+        if (liveData !== newLiveData) {
             if (liveData != null) {
                 result.removeSource(liveData!!)
             }
@@ -133,7 +140,7 @@ fun  LiveData.switchMap(
                 result.addSource(liveData!!) { y -> result.setValue(y) }
             }
         }
-    })
+    }
     return result
 }
 
@@ -193,4 +200,4 @@ fun  LiveData.distinctUntilChanged(): LiveData {
         }
     }
     return outputLiveData
-}
\ No newline at end of file
+}