Skip to content

Commit

Permalink
Implement to provide ComposeCoroutineScope
Browse files Browse the repository at this point in the history
  • Loading branch information
mori-atsushi committed Mar 13, 2023
1 parent d7d95a5 commit adba37c
Show file tree
Hide file tree
Showing 13 changed files with 226 additions and 15 deletions.
1 change: 1 addition & 0 deletions compose/koject-compose-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ kotlin {
dependencies {
api(project(":koject-core"))
implementation(compose.runtime)
implementation(libs.kotlinx.coroutines.core)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,25 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import com.moriatsushi.koject.ExperimentalKojectApi
import com.moriatsushi.koject.component.ComponentExtras
import kotlinx.coroutines.CoroutineScope

@OptIn(ExperimentalKojectApi::class)
internal actual class ComposeComponentExtras(
@ComposeContext
val context: Context,
) : ComponentExtras<ComposeComponent>
private val coroutineScopeHolder: CoroutineScopeHolder,
) : ComponentExtras<ComposeComponent> {
@ComposeCoroutineScope
actual val coroutineScope: CoroutineScope
get() = coroutineScopeHolder.coroutineScope
}

@PublishedApi
@Composable
internal actual fun composeComponentExtras(): ComposeComponentExtras {
val context = LocalContext.current
val coroutineScopeHolder = rememberCoroutineScopeHolder()
return remember(context) {
ComposeComponentExtras(context)
ComposeComponentExtras(context, coroutineScopeHolder)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ package com.moriatsushi.koject.compose
import androidx.compose.runtime.Composable
import com.moriatsushi.koject.ExperimentalKojectApi
import com.moriatsushi.koject.component.ComponentExtras
import kotlinx.coroutines.CoroutineScope

@OptIn(ExperimentalKojectApi::class)
internal expect class ComposeComponentExtras : ComponentExtras<ComposeComponent>
internal expect class ComposeComponentExtras : ComponentExtras<ComposeComponent> {
@ComposeCoroutineScope
val coroutineScope: CoroutineScope
}

@Composable
@PublishedApi
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.moriatsushi.koject.compose

import androidx.compose.runtime.rememberCoroutineScope
import com.moriatsushi.koject.Qualifier
import kotlinx.coroutines.CoroutineScope

/**
* Get a Composable [CoroutineScope] that the same as [rememberCoroutineScope].
*/
@Qualifier
@Retention(AnnotationRetention.BINARY)
@MustBeDocumented
annotation class ComposeCoroutineScope
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.moriatsushi.koject.compose

import androidx.compose.runtime.Composable
import androidx.compose.runtime.RememberObserver
import androidx.compose.runtime.remember
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel

internal interface CoroutineScopeHolder {
val coroutineScope: CoroutineScope
}

private class CoroutineScopeHolderImpl(
private val coroutineContext: CoroutineContext,
) : CoroutineScopeHolder, RememberObserver {
private var _coroutineScope: CoroutineScope? = null
override val coroutineScope: CoroutineScope
get() = _coroutineScope ?: CoroutineScope(coroutineContext).also {
_coroutineScope = it
}

override fun onRemembered() {
// no op
}

override fun onForgotten() {
_coroutineScope?.cancel()
}

override fun onAbandoned() {
_coroutineScope?.cancel()
}
}

@Composable
internal fun rememberCoroutineScopeHolder(): CoroutineScopeHolder {
return remember {
CoroutineScopeHolderImpl(
Job() + Dispatchers.Main.immediate,
)
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
package com.moriatsushi.koject.compose

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.moriatsushi.koject.ExperimentalKojectApi
import com.moriatsushi.koject.component.ComponentExtras
import kotlinx.coroutines.CoroutineScope

@OptIn(ExperimentalKojectApi::class)
internal actual class ComposeComponentExtras : ComponentExtras<ComposeComponent>

private val instance = ComposeComponentExtras()
internal actual class ComposeComponentExtras(
private val coroutineScopeHolder: CoroutineScopeHolder,
) : ComponentExtras<ComposeComponent> {
@ComposeCoroutineScope
actual val coroutineScope: CoroutineScope
get() = coroutineScopeHolder.coroutineScope
}

@PublishedApi
@Composable
internal actual fun composeComponentExtras(): ComposeComponentExtras {
return instance
val coroutineScopeHolder = rememberCoroutineScopeHolder()
return remember {
ComposeComponentExtras(coroutineScopeHolder)
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
package com.moriatsushi.koject.compose

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.moriatsushi.koject.ExperimentalKojectApi
import com.moriatsushi.koject.component.ComponentExtras
import kotlinx.coroutines.CoroutineScope

@OptIn(ExperimentalKojectApi::class)
internal actual class ComposeComponentExtras : ComponentExtras<ComposeComponent>

private val instance = ComposeComponentExtras()
internal actual class ComposeComponentExtras(
private val coroutineScopeHolder: CoroutineScopeHolder,
) : ComponentExtras<ComposeComponent> {
@ComposeCoroutineScope
actual val coroutineScope: CoroutineScope
get() = coroutineScopeHolder.coroutineScope
}

@PublishedApi
@Composable
internal actual fun composeComponentExtras(): ComposeComponentExtras {
return instance
val coroutineScopeHolder = rememberCoroutineScopeHolder()
return remember {
ComposeComponentExtras(coroutineScopeHolder)
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
package com.moriatsushi.koject.compose

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.moriatsushi.koject.ExperimentalKojectApi
import com.moriatsushi.koject.component.ComponentExtras
import kotlinx.coroutines.CoroutineScope

@OptIn(ExperimentalKojectApi::class)
internal actual class ComposeComponentExtras : ComponentExtras<ComposeComponent>

private val instance = ComposeComponentExtras()
internal actual class ComposeComponentExtras(
private val coroutineScopeHolder: CoroutineScopeHolder,
) : ComponentExtras<ComposeComponent> {
@ComposeCoroutineScope
actual val coroutineScope: CoroutineScope
get() = coroutineScopeHolder.coroutineScope
}

@PublishedApi
@Composable
internal actual fun composeComponentExtras(): ComposeComponentExtras {
return instance
val coroutineScopeHolder = rememberCoroutineScopeHolder()
return remember {
ComposeComponentExtras(coroutineScopeHolder)
}
}
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ compose-multiplatform = "1.3.1"
kotlin = "1.8.10"
kotlin-compile-testing = "1.5.0"
kotlinpoet = "1.12.0"
kotlinx-coroutines = "1.6.4"
ksp = "1.8.10-1.0.9"
ktlint = "0.48.1"
mockito-kotlin = "4.1.0"
Expand All @@ -33,6 +34,7 @@ androidx-test-core = { group = "androidx.test", name = "core-ktx", version.ref =
androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit-ktx", version.ref = "androidx-test-ext-junit" }
kotlin-compile-testing-ksp = { group = "com.github.tschuchortdev", name = "kotlin-compile-testing-ksp", version.ref = "kotlin-compile-testing" }
kotlinpoet-ksp = { group = "com.squareup", name = "kotlinpoet-ksp", version.ref = "kotlinpoet" }
kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
ksp-processor-api = { group = "com.google.devtools.ksp", name = "symbol-processing-api", version.ref = "ksp" }
mockito-kotlin = { group = "org.mockito.kotlin", name = "mockito-kotlin", version.ref = "mockito-kotlin" }
robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" }
Expand Down
1 change: 1 addition & 0 deletions integration-test/compose/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ kotlin {
dependencies {
api(project(":compose:koject-compose-core"))
implementation(compose.runtime)
implementation(libs.kotlinx.coroutines.core)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package com.moriatsushi.koject.integrationtest.compose
import com.moriatsushi.koject.Provides
import com.moriatsushi.koject.Singleton
import com.moriatsushi.koject.compose.ComposeComponent
import com.moriatsushi.koject.compose.ComposeCoroutineScope
import kotlinx.coroutines.CoroutineScope

@Provides
class CommonClass
Expand All @@ -20,3 +22,10 @@ class ForComposeClass
class ForComposeHolder(
value: ForComposeClass,
)

@ComposeComponent
@Provides
class ForComposeWithCoroutineScope(
@ComposeCoroutineScope
val scope: CoroutineScope,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.moriatsushi.koject.integrationtest.compose

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.moriatsushi.koject.Koject
import com.moriatsushi.koject.compose.rememberInject
import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import kotlinx.coroutines.isActive
import org.jetbrains.compose.web.testutils.ComposeWebExperimentalTestsApi
import org.jetbrains.compose.web.testutils.runTest

@OptIn(ComposeWebExperimentalTestsApi::class)
class ComposeCoroutineScopeTest {
@Test
fun successInject() = Koject.runTest {
runTest {
var value: ForComposeWithCoroutineScope? = null

composition {
value = rememberInject()
}

assertNotNull(value)
}
}

@Test
fun cancelScope_whenHidden() = Koject.runTest {
runTest {
var visible by mutableStateOf(true)
var value: ForComposeWithCoroutineScope? = null

composition {
if (visible) {
value = rememberInject()
}
}

assertTrue(value!!.scope.isActive)

visible = false
waitForRecompositionComplete()

assertFalse(value!!.scope.isActive)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.moriatsushi.koject.integrationtest.compose

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.moriatsushi.koject.Koject
import com.moriatsushi.koject.compose.rememberInject
import com.moriatsushi.koject.integrationtest.compose.junit.RunWith
import com.moriatsushi.koject.integrationtest.compose.junit.UITestRunner
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import kotlinx.coroutines.isActive
import org.junit.Rule
import org.junit.Test

@RunWith(UITestRunner::class)
class ComposeCoroutineScopeTest {
@get:Rule
val composeTestRule = createComposeRule()

@Test
fun successInject() = Koject.runTest {
var value: ForComposeWithCoroutineScope? = null

composeTestRule.setContent {
value = rememberInject()
}

assertNotNull(value)
}

@Test
fun cancelScope_whenHidden() = Koject.runTest {
var visible by mutableStateOf(true)
var value: ForComposeWithCoroutineScope? = null

composeTestRule.setContent {
if (visible) {
value = rememberInject()
}
}

assertTrue(value!!.scope.isActive)

visible = false
composeTestRule.waitForIdle()

assertFalse(value!!.scope.isActive)
}
}

0 comments on commit adba37c

Please sign in to comment.