-
Notifications
You must be signed in to change notification settings - Fork 61
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create interop-coroutines module (missing tests)
- Loading branch information
Showing
8 changed files
with
273 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
plugins { | ||
id("com.bumble.appyx.multiplatform") | ||
id("com.android.library") | ||
id("appyx-publish-multiplatform") | ||
} | ||
|
||
appyx { | ||
androidNamespace.set("com.bumble.appyx.utils.interop.coroutines") | ||
} | ||
|
||
kotlin { | ||
androidTarget { | ||
publishLibraryVariants("release") | ||
} | ||
jvm("desktop") { | ||
compilations.all { | ||
kotlinOptions.jvmTarget = libs.versions.jvmTarget.get() | ||
} | ||
} | ||
js(IR) { | ||
// Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942 | ||
moduleName = "appyx-utils-coroutines" | ||
browser() | ||
} | ||
|
||
iosX64() | ||
iosArm64() | ||
iosSimulatorArm64() | ||
|
||
sourceSets { | ||
val commonMain by getting { | ||
dependencies { | ||
api(project(":appyx-navigation:appyx-navigation")) | ||
} | ||
} | ||
val commonTest by getting { | ||
dependencies { | ||
implementation(kotlin("test")) | ||
implementation(libs.kotlin.coroutines.test) | ||
} | ||
} | ||
val androidMain by getting | ||
val desktopMain by getting | ||
val jsMain by getting | ||
val iosX64Main by getting | ||
val iosArm64Main by getting | ||
val iosSimulatorArm64Main by getting | ||
val iosMain by creating { | ||
dependsOn(commonMain) | ||
iosX64Main.dependsOn(this) | ||
iosArm64Main.dependsOn(this) | ||
iosSimulatorArm64Main.dependsOn(this) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<issues format="6" by="lint 7.3.1" type="baseline" client="gradle" dependencies="false" name="AGP (7.3.1)" variant="all" version="7.3.1"> | ||
|
||
</issues> |
8 changes: 8 additions & 0 deletions
8
...rc/commonMain/kotlin/com/bumble/appyx/utils/interop/coroutines/connectable/Connectable.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package com.bumble.appyx.utils.interop.coroutines.connectable | ||
|
||
import kotlinx.coroutines.flow.MutableSharedFlow | ||
|
||
interface Connectable<Input, Output> { | ||
val input: MutableSharedFlow<Input> | ||
val output: MutableSharedFlow<Output> | ||
} |
113 changes: 113 additions & 0 deletions
113
.../commonMain/kotlin/com/bumble/appyx/utils/interop/coroutines/connectable/NodeConnector.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
package com.bumble.appyx.utils.interop.coroutines.connectable | ||
|
||
import com.bumble.appyx.navigation.lifecycle.Lifecycle | ||
import com.bumble.appyx.navigation.plugin.NodeLifecycleAware | ||
import kotlinx.coroutines.CoroutineScope | ||
import kotlinx.coroutines.Dispatchers | ||
import kotlinx.coroutines.ExperimentalCoroutinesApi | ||
import kotlinx.coroutines.Job | ||
import kotlinx.coroutines.flow.FlowCollector | ||
import kotlinx.coroutines.flow.MutableSharedFlow | ||
import kotlinx.coroutines.flow.StateFlow | ||
import kotlinx.coroutines.launch | ||
|
||
class NodeConnector<Input, Output : Any> : Connectable<Input, Output>, NodeLifecycleAware { | ||
|
||
override fun onCreate(lifecycle: Lifecycle) { | ||
flushInput() | ||
flushOutput() | ||
} | ||
|
||
// region Input | ||
private fun flushInput() { | ||
if (!isInputFlushed) { | ||
val coroutineScope = CoroutineScope(Dispatchers.Default + Job()) | ||
_inputReplayCache.forEach { | ||
coroutineScope.launch { _input.emit(it) } | ||
} | ||
} | ||
isInputFlushed = true | ||
_inputReplayCache.clear() | ||
} | ||
|
||
private var isInputFlushed = false | ||
private val _input = MutableSharedFlow<Input>() | ||
private val _inputReplayCache = mutableListOf<Input>() | ||
override val input: MutableSharedFlow<Input> = object : MutableSharedFlow<Input> { | ||
|
||
override val replayCache: List<Input> = _inputReplayCache | ||
|
||
override val subscriptionCount: StateFlow<Int> = _input.subscriptionCount | ||
|
||
@OptIn(ExperimentalCoroutinesApi::class) | ||
override fun resetReplayCache() { | ||
_inputReplayCache.clear() | ||
} | ||
|
||
override fun tryEmit(value: Input): Boolean = | ||
if (isInputFlushed) { | ||
_input.tryEmit(value) | ||
} else { | ||
_inputReplayCache.add(value) | ||
} | ||
|
||
override suspend fun emit(value: Input) { | ||
if (isInputFlushed) { | ||
_input.emit(value) | ||
} else { | ||
_inputReplayCache.add(value) | ||
} | ||
} | ||
|
||
override suspend fun collect(collector: FlowCollector<Input>): Nothing = | ||
_input.collect(collector) | ||
} | ||
// endregion | ||
|
||
// region Output | ||
private fun flushOutput() { | ||
if (!isOutputFlushed) { | ||
val coroutineScope = CoroutineScope(Dispatchers.Default + Job()) | ||
_outputReplayCache.forEach { | ||
coroutineScope.launch { _output.emit(it) } | ||
} | ||
} | ||
isOutputFlushed = true | ||
_outputReplayCache.clear() | ||
} | ||
|
||
private var isOutputFlushed = false | ||
private val _output = MutableSharedFlow<Output>() | ||
private val _outputReplayCache = mutableListOf<Output>() | ||
override val output: MutableSharedFlow<Output> = object : MutableSharedFlow<Output> { | ||
|
||
override val replayCache: List<Output> = _outputReplayCache | ||
|
||
override val subscriptionCount: StateFlow<Int> = _output.subscriptionCount | ||
|
||
@OptIn(ExperimentalCoroutinesApi::class) | ||
override fun resetReplayCache() { | ||
_outputReplayCache.clear() | ||
} | ||
|
||
override fun tryEmit(value: Output): Boolean = | ||
if (isOutputFlushed) { | ||
_output.tryEmit(value) | ||
} else { | ||
_outputReplayCache.add(value) | ||
} | ||
|
||
override suspend fun emit(value: Output) { | ||
if (isOutputFlushed) { | ||
_output.emit(value) | ||
} else { | ||
_outputReplayCache.add(value) | ||
} | ||
} | ||
|
||
override suspend fun collect(collector: FlowCollector<Output>): Nothing = | ||
_output.collect(collector) | ||
} | ||
// endregion | ||
|
||
} |
13 changes: 13 additions & 0 deletions
13
...rc/commonMain/kotlin/com/bumble/appyx/utils/interop/coroutines/plugin/DisposeOnDestroy.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package com.bumble.appyx.utils.interop.coroutines.plugin | ||
|
||
import com.bumble.appyx.interactions.core.plugin.Plugin | ||
import com.bumble.appyx.navigation.plugin.Destroyable | ||
import kotlinx.coroutines.Job | ||
|
||
private class DisposeOnDestroy(private val jobs: List<Job>) : Destroyable { | ||
override fun destroy() { | ||
jobs.forEach { it.cancel() } | ||
} | ||
} | ||
|
||
fun disposeOnDestroyPlugin(vararg jobs: Job): Plugin = DisposeOnDestroy(jobs.toList()) |
34 changes: 34 additions & 0 deletions
34
...onMain/kotlin/com/bumble/appyx/utils/interop/coroutines/store/RetainedInstanceStoreExt.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
package com.bumble.appyx.utils.interop.coroutines.store | ||
|
||
import com.bumble.appyx.navigation.modality.NodeContext | ||
import com.bumble.appyx.navigation.store.RetainedInstanceStore | ||
import com.bumble.appyx.navigation.store.getRetainedInstance | ||
import kotlinx.coroutines.Job | ||
|
||
/** | ||
* Obtains or creates an instance of a class via the [get] extension. | ||
* The Job will be cancelled when the disposer function is called. | ||
*/ | ||
inline fun <reified T : Job> RetainedInstanceStore.getJob( | ||
storeId: String, | ||
key: String, | ||
noinline factory: () -> T | ||
): T = get( | ||
storeId = storeId, | ||
disposer = { it.cancel() }, | ||
factory = factory, | ||
key = key, | ||
) | ||
|
||
/** | ||
* Obtains or creates an instance of a class via the [getRetainedInstance] extension. | ||
* The Job will be cancelled when the disposer function is called. | ||
*/ | ||
inline fun <reified T : Job> NodeContext.getRetainedDisposable( | ||
key: String, | ||
noinline factory: () -> T | ||
) = getRetainedInstance( | ||
key = key, | ||
disposer = { it.cancel() }, | ||
factory = factory, | ||
) |
45 changes: 45 additions & 0 deletions
45
...kotlin/com/bumble/appyx/utils/interop/coroutines/plugin/CoroutinesDisposeOnDestroyTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
package com.bumble.appyx.utils.interop.coroutines.plugin | ||
|
||
import com.bumble.appyx.navigation.plugin.Destroyable | ||
import kotlinx.coroutines.Job | ||
import kotlin.test.assertFalse | ||
import kotlin.test.assertIs | ||
import kotlin.test.assertTrue | ||
import kotlin.test.Test | ||
|
||
internal class CoroutinesDisposeOnDestroyTest { | ||
@Test | ||
fun `WHEN dispose on destroy plugin created THEN verify is destroyable type`() { | ||
assertIs<Destroyable>(disposeOnDestroyPlugin()) | ||
} | ||
|
||
@Test | ||
fun `GIVEN dispose on destroy plugin created with job WHEN destroy THEN job is cancelled`() { | ||
val job = Job() | ||
val disposeOnDestroyPlugin = disposeOnDestroyPlugin(job) | ||
|
||
(disposeOnDestroyPlugin as Destroyable).destroy() | ||
|
||
assertTrue(job.isCancelled) | ||
} | ||
|
||
@Test | ||
fun `GIVEN dispose on destroy plugin created with multiple jobs WHEN destroy THEN all jobs are cancelled`() { | ||
val job1 = Job() | ||
val job2 = Job() | ||
val disposeOnDestroyPlugin = disposeOnDestroyPlugin(job1, job2) | ||
|
||
(disposeOnDestroyPlugin as Destroyable).destroy() | ||
|
||
assertTrue(job1.isCancelled) | ||
assertTrue(job2.isCancelled) | ||
} | ||
|
||
@Test | ||
fun `WHEN dispose on destroy plugin created with job THEN job is not cancelled`() { | ||
val job = Job() | ||
disposeOnDestroyPlugin(job) | ||
|
||
assertFalse(job.isCancelled) | ||
} | ||
} |