Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[compose] Implement to provide ComposeCoroutineScope #160

Merged
merged 2 commits into from
Mar 13, 2023
Merged
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
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 @@ -6,6 +6,10 @@ import com.moriatsushi.koject.component.Component

/**
* Mark it as a type to inject into [Composable].
*
* Additional available types:
* * [kotlinx.coroutines.CoroutineScope] with @[ComposeCoroutineScope]
* * android.content.Context with @ComposeContext (android only)
*/
@OptIn(ExperimentalKojectApi::class)
@Component
Expand Down
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)
}
}