Skip to content

Commit

Permalink
Merge pull request #121 from Dmitriy1892/lifecycle-coroutines
Browse files Browse the repository at this point in the history
Lifecycle-aware extensions for kotlin coroutines and flows were added.
  • Loading branch information
arkivanov committed Nov 18, 2023
2 parents 8e199ee + 729d6e6 commit 244119f
Show file tree
Hide file tree
Showing 9 changed files with 433 additions and 1 deletion.
5 changes: 4 additions & 1 deletion deps.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
essenty = "1.3.0-alpha04"
kotlin = "1.9.20"
kotlinxBinaryCompatibilityValidator = "0.13.2"
kotlinxCoroutines = "1.7.3"
detektGradlePlugin = "1.23.3"
junit = "4.13.2"
androidGradle = "8.0.2"
Expand All @@ -17,9 +18,11 @@ parcelizeDarwin = "0.2.3"
[libraries]

kotlin-kotlinGradlePlug = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }

kotlinx-binaryCompatibilityValidator = { group = "org.jetbrains.kotlinx", name = "binary-compatibility-validator", version.ref = "kotlinxBinaryCompatibilityValidator" }

kotlinx-coroutinesCore = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" }
kotlinx-coroutinesTest = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" }

detekt-gradleDetektPlug = { group = "io.gitlab.arturbosch.detekt", name = "detekt-gradle-plugin", version.ref = "detektGradlePlugin" }

android-gradle = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradle" }
Expand Down
1 change: 1 addition & 0 deletions lifecycle-coroutines/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
12 changes: 12 additions & 0 deletions lifecycle-coroutines/api/android/lifecycle-coroutines.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
public final class com/arkivanov/essenty/lifecycle/coroutines/FlowWithLifecycleKt {
public static final fun flowWithLifecycle (Lkotlinx/coroutines/flow/Flow;Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/flow/Flow;
public static synthetic fun flowWithLifecycle$default (Lkotlinx/coroutines/flow/Flow;Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow;
public static final fun withLifecycle (Lkotlinx/coroutines/flow/Flow;Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/flow/Flow;
public static synthetic fun withLifecycle$default (Lkotlinx/coroutines/flow/Flow;Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow;
}

public final class com/arkivanov/essenty/lifecycle/coroutines/RepeatOnLifecycleKt {
public static final fun repeatOnLifecycle (Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun repeatOnLifecycle$default (Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
}

12 changes: 12 additions & 0 deletions lifecycle-coroutines/api/jvm/lifecycle-coroutines.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
public final class com/arkivanov/essenty/lifecycle/coroutines/FlowWithLifecycleKt {
public static final fun flowWithLifecycle (Lkotlinx/coroutines/flow/Flow;Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/flow/Flow;
public static synthetic fun flowWithLifecycle$default (Lkotlinx/coroutines/flow/Flow;Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow;
public static final fun withLifecycle (Lkotlinx/coroutines/flow/Flow;Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/flow/Flow;
public static synthetic fun withLifecycle$default (Lkotlinx/coroutines/flow/Flow;Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow;
}

public final class com/arkivanov/essenty/lifecycle/coroutines/RepeatOnLifecycleKt {
public static final fun repeatOnLifecycle (Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun repeatOnLifecycle$default (Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
}

35 changes: 35 additions & 0 deletions lifecycle-coroutines/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import com.arkivanov.gradle.bundle
import com.arkivanov.gradle.setupBinaryCompatibilityValidator
import com.arkivanov.gradle.setupMultiplatform
import com.arkivanov.gradle.setupPublication
import com.arkivanov.gradle.setupSourceSets

plugins {
id("kotlin-multiplatform")
id("com.android.library")
id("com.arkivanov.gradle.setup")
}

setupMultiplatform()
setupPublication()
setupBinaryCompatibilityValidator()

android {
namespace = "com.arkivanov.essenty.lifecycle.coroutines"
}

kotlin {
setupSourceSets {
val android by bundle()

common.main.dependencies {
implementation(project(":utils-internal"))
implementation(project(":lifecycle"))
implementation(deps.kotlinx.coroutinesCore)
}

common.test.dependencies {
implementation(deps.kotlinx.coroutinesTest)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.arkivanov.essenty.lifecycle.coroutines

import com.arkivanov.essenty.lifecycle.Lifecycle
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlin.coroutines.CoroutineContext

/**
* Start [Flow] collecting when [Lifecycle.State] is at least as [minActiveState].
* It stopped collecting if "opposite" [Lifecycle.State] will appear.
*/
fun <T> Flow<T>.withLifecycle(
lifecycle: Lifecycle,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
context: CoroutineContext = Dispatchers.Main
): Flow<T> = callbackFlow {
lifecycle.repeatOnLifecycle(minActiveState, context) {
this@withLifecycle.collect {
send(it)
}
}
close()
}

@Deprecated(
message = "Use 'withLifecycle' instead",
replaceWith = ReplaceWith("withLifecycle(lifecycle, minActiveState)"),
level = DeprecationLevel.ERROR
)
fun <T> Flow<T>.flowWithLifecycle(
lifecycle: Lifecycle,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
context: CoroutineContext = Dispatchers.Main
): Flow<T> {
return withLifecycle(lifecycle, minActiveState, context)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package com.arkivanov.essenty.lifecycle.coroutines

import com.arkivanov.essenty.lifecycle.Lifecycle
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.resume

/**
* Repeat invocation [block] every time that passed [minActiveState] appears.
* Work of passed [block] finished when "opposite" [Lifecycle.State] will appear.
*
* Note: This function works like a terminal operator and must be called in assembly coroutine.
*/
suspend fun Lifecycle.repeatOnLifecycle(
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
context: CoroutineContext = Dispatchers.Main,
block: suspend CoroutineScope.() -> Unit
) {
require(minActiveState != Lifecycle.State.INITIALIZED) {
"repeatOnEssentyLifecycle cannot start work with the INITIALIZED lifecycle state."
}

if (this.state == Lifecycle.State.DESTROYED) {
return
}

coroutineScope {
withContext(context) {
if (this@repeatOnLifecycle.state == Lifecycle.State.DESTROYED) {
return@withContext
}

var callback: Lifecycle.Callbacks? = null
var job: Job? = null
val mutex = Mutex()

try {
suspendCancellableCoroutine { cont ->
callback = createLifecycleAwareCallback(
startState = minActiveState,
onStateAppear = {
job = launch {
mutex.withLock {
block()
}
}
},
onStateDisappear = {
job?.cancel()
job = null
},
onDestroy = {
cont.resume(Unit)
},
)

this@repeatOnLifecycle.subscribe(requireNotNull(callback))
}
} finally {
job?.cancel()
job = null
callback?.let {
this@repeatOnLifecycle.unsubscribe(it)
}
callback = null
}
}
}
}

/**
* Creates lifecycle aware [Lifecycle.Callbacks] interface instance.
*
* @param startState [Lifecycle.State] that [onStateAppear] block must be called from
* @param onStateAppear block of code that will be executed when the [Lifecycle.State] was equal [startState]
* @param onStateDisappear block of code that will be executed when the [Lifecycle.State] was equal to opposite [startState]
* @param onDestroy block of code that will be executed when the [Lifecycle.State] was equal [Lifecycle.State.DESTROYED]
*
* @return [Lifecycle.Callbacks]
*/
private fun createLifecycleAwareCallback(
startState: Lifecycle.State,
onStateAppear: () -> Unit,
onStateDisappear: () -> Unit,
onDestroy: () -> Unit,
): Lifecycle.Callbacks = object : Lifecycle.Callbacks {

override fun onCreate() {
launchIfState(Lifecycle.State.CREATED)
}

override fun onStart() {
launchIfState(Lifecycle.State.STARTED)
}

override fun onResume() {
launchIfState(Lifecycle.State.RESUMED)
}

override fun onPause() {
closeIfState(Lifecycle.State.RESUMED)
}

override fun onStop() {
closeIfState(Lifecycle.State.STARTED)
}

override fun onDestroy() {
closeIfState(Lifecycle.State.CREATED)
onDestroy()
}

private fun launchIfState(state: Lifecycle.State) {
if (startState == state) {
onStateAppear()
}
}

private fun closeIfState(state: Lifecycle.State) {
if (startState == state) {
onStateDisappear()
}
}
}
Loading

0 comments on commit 244119f

Please sign in to comment.