Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,7 @@ import com.bumble.appyx.navigation.lifecycle.NodeLifecycle
import com.bumble.appyx.navigation.lifecycle.NodeLifecycleImpl
import com.bumble.appyx.navigation.modality.AncestryInfo
import com.bumble.appyx.navigation.platform.PlatformBackHandler
import com.bumble.appyx.navigation.plugin.Destroyable
import com.bumble.appyx.navigation.plugin.NodeLifecycleAware
import com.bumble.appyx.navigation.plugin.NodeReadyObserver
import com.bumble.appyx.navigation.plugin.UpNavigationHandler
import com.bumble.appyx.navigation.plugin.plugins
import com.bumble.appyx.navigation.plugin.*

Check warning

Code scanning / detekt

Wildcard imports should be replaced with imports using fully qualified class names. Wildcard imports can lead to naming conflicts. A library update can introduce naming clashes with your classes which results in compilation errors.

com.bumble.appyx.navigation.plugin.* is a wildcard import. Replace it with fully qualified imports.
import com.bumble.appyx.navigation.store.RetainedInstanceStore
import com.bumble.appyx.utils.multiplatform.BuildFlags
import com.bumble.appyx.utils.multiplatform.SavedStateMap
Expand Down Expand Up @@ -153,6 +149,7 @@ abstract class Node<NavTarget : Any>(
updateLifecycleState(Lifecycle.State.CREATED)
plugins<NodeReadyObserver<Node<*>>>().forEach { it.init(this) }
plugins<NodeLifecycleAware>().forEach { it.onCreate(lifecycle) }
plugins<Tooling>().forEach { it.onSetupTooling(lifecycle) }
childNodeCreationManager.launch(this)
childNodeLifecycleManager.launch()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ interface NodeLifecycleAware : Plugin {
fun onCreate(lifecycle: Lifecycle) {}
}

interface Tooling : Plugin {
fun onSetupTooling(lifecycle: Lifecycle) {}
}

interface UpNavigationHandler : Plugin {
fun handleUpNavigation(): Boolean = false
}
Expand Down
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ include(
":demos:sandbox-appyx-navigation:web",
":ksp:mutable-ui-processor",
":utils:customisations",
":utils:interop-coroutines",
":utils:interop-ribs",
":utils:interop-rx2",
":utils:interop-rx3",
Expand Down
55 changes: 55 additions & 0 deletions utils/interop-coroutines/build.gradle.kts
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)
}
}
}
4 changes: 4 additions & 0 deletions utils/interop-coroutines/lint-baseline.xml
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>
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>
}
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

}
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())
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,
)
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)
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.bumble.appyx.utils.interop.rx2.connectable

import com.bumble.appyx.navigation.plugin.NodeLifecycleAware
import com.bumble.appyx.navigation.plugin.Tooling
import com.jakewharton.rxrelay2.Relay

interface Connectable<Input, Output> : NodeLifecycleAware {
interface Connectable<Input, Output> : Tooling {
val input: Relay<Input>
val output: Relay<Output>
}
Loading