Skip to content

Instantly share code, notes, and snippets.

@pberdnik
Last active April 16, 2025 08:20
Show Gist options
  • Save pberdnik/3d052989f5299a78378a640d1920b281 to your computer and use it in GitHub Desktop.
Save pberdnik/3d052989f5299a78378a640d1920b281 to your computer and use it in GitHub Desktop.
Koin + Decompose
package ru.pberdnik.example.core.di_extension
import co.touchlab.kermit.Logger
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.ComponentContextFactory
import com.arkivanov.decompose.GenericComponentContext
import com.arkivanov.essenty.backhandler.BackHandlerOwner
import com.arkivanov.essenty.instancekeeper.InstanceKeeper
import com.arkivanov.essenty.instancekeeper.InstanceKeeperOwner
import com.arkivanov.essenty.instancekeeper.getOrCreate
import com.arkivanov.essenty.lifecycle.LifecycleOwner
import com.arkivanov.essenty.statekeeper.StateKeeperOwner
import org.koin.core.component.KoinScopeComponent
import org.koin.core.scope.Scope
import org.koin.mp.KoinPlatform.getKoin
interface AppComponentContext : KoinScopeComponent,
GenericComponentContext<AppComponentContext> {
val componentContext: ComponentContext
}
open class AppComponentContextWrapper(
override val componentContext: ComponentContext,
private val retainedKoinScope: RetainedKoinScope,
) : AppComponentContext,
LifecycleOwner by componentContext,
StateKeeperOwner by componentContext,
InstanceKeeperOwner by componentContext,
BackHandlerOwner by componentContext {
override val scope: Scope
get() = retainedKoinScope.scope
override val componentContextFactory: ComponentContextFactory<AppComponentContext> =
ComponentContextFactory { lifecycle, stateKeeper, instanceKeeper, backHandler ->
val ctx = componentContext.componentContextFactory(
lifecycle,
stateKeeper,
instanceKeeper,
backHandler
)
AppComponentContextWrapper(ctx, retainedKoinScope)
}
}
inline fun <reified T : Any> AppComponentContext.withKoin(): AppComponentContext {
val retainedKoinScope: RetainedKoinScope = instanceKeeper.getOrCreate { RetainedKoinScope() }
retainedKoinScope.initScope<T>()
retainedKoinScope.scope.linkTo(scope)
Logger.i("koinScopes") { "withKoin(); scope ${retainedKoinScope.scope.id} is linked to ${scope.id}; ${retainedKoinScope.scope::class.simpleName}\$${retainedKoinScope.scope.hashCode()} and ${scope::class.simpleName}\$${scope.hashCode()}" }
return AppComponentContextWrapper(this.componentContext, retainedKoinScope)
}
inline fun <reified T : Any> ComponentContext.withKoin(): AppComponentContext {
val retainedKoinScope: RetainedKoinScope = instanceKeeper.getOrCreate { RetainedKoinScope() }
retainedKoinScope.initScope<T>()
Logger.i("koinScopes") { "withKoin(); ROOT scope is ${retainedKoinScope.scope.id} ${retainedKoinScope.scope::class.simpleName}\$${retainedKoinScope.scope.hashCode()}" }
return AppComponentContextWrapper(this, retainedKoinScope)
}
class RetainedKoinScope : InstanceKeeper.Instance {
var nullableScope: Scope? = null
val scope get() = nullableScope!!
inline fun <reified T : Any> initScope() {
if (nullableScope != null) return
nullableScope = getKoin().getOrCreateScope<T>(
T::class.qualifiedName ?: error("${T::class} qualifiedName is null unexpectedly")
)
}
override fun onDestroy() {
Logger.i("koinScopes") { "DESTROY ${scope.id} ${scope::class.simpleName}\$${scope.hashCode()}" }
scope.close()
}
}
package ru.pberdnik.example.features.di
import org.koin.core.module.dsl.scopedOf
import org.koin.dsl.module
import ru.pberdnik.example.features.bottomnav.navigation.ParentComponent
import ru.pberdnik.example.features.home.navigation.ChildComponent
val myModule = module {
scope<ParentComponent> {
scopedOf(::ParentScoped)
}
scope<ChildComponent> {
scopedOf(::ChildScoped)
}
}
class ParentScoped
class ChildScoped
package ru.pberdnik.example.navigation
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.router.stack.ChildStack
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.childStack
import com.arkivanov.decompose.value.Value
import kotlinx.serialization.Serializable
import ru.pberdnik.example.core.di_extension.withKoin
import ru.pberdnik.example.features.bottomnav.navigation.ParentComponent
import ru.pberdnik.example.features.bottomnav.navigation.DefaultParentComponent
import ru.pberdnik.example.navigation.RootComponent.Child
import ru.pberdnik.example.navigation.RootComponent.Config
interface RootComponent {
val stack: Value<ChildStack<Config, Child>>
sealed class Child {
class BottomNavigation(val component: ParentComponent) : Child()
}
@Serializable
sealed class Config {
@Serializable
data object BottomNavigation : Config()
}
}
class DefaultRootComponent(
componentContext: ComponentContext
) : RootComponent, ComponentContext by componentContext {
private val navigation = StackNavigation<Config>()
override val stack: Value<ChildStack<Config, Child>> = childStack(
source = navigation,
serializer = Config.serializer(),
initialConfiguration = Config.BottomNavigation,
handleBackButton = true,
childFactory = ::createChild
)
private fun createChild(
config: Config,
componentContext: ComponentContext
): Child {
return when (config) {
Config.BottomNavigation -> Child.BottomNavigation(
DefaultParentComponent(
componentContext.withKoin<ParentComponent>()
)
)
}
}
}
package ru.pberdnik.example.features.bottomnav.navigation
import co.touchlab.kermit.Logger
import com.arkivanov.decompose.router.stack.ChildStack
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.childStack
import com.arkivanov.decompose.router.stack.pushToFront
import com.arkivanov.decompose.value.Value
import kotlinx.serialization.Serializable
import org.koin.core.component.get
import ru.pberdnik.example.core.di_extension.AppComponentContext
import ru.pberdnik.example.core.di_extension.withKoin
import ru.pberdnik.example.features.bottomnav.navigation.ParentComponent.Child
import ru.pberdnik.example.features.bottomnav.navigation.ParentComponent.Config
import ru.pberdnik.example.features.di.ChildScoped
import ru.pberdnik.example.features.di.ParentScoped
import ru.pberdnik.example.features.home.navigation.ChildComponent
import ru.pberdnik.example.features.home.navigation.DefaultChildComponent
interface ParentComponent {
val stack: Value<ChildStack<Config, Child>>
fun selectTab(screen: Config)
sealed class Child {
class MyChild(val component: ChildComponent) : Child()
}
@Serializable
sealed class Config {
@Serializable
data object Child : Config()
}
}
class DefaultParentComponent(
context: AppComponentContext
) : ParentComponent, AppComponentContext by context {
private val navigation = StackNavigation<Config>()
override val stack: Value<ChildStack<Config, Child>> = childStack(
source = navigation,
serializer = Config.serializer(),
initialConfiguration = Config.Child,
handleBackButton = true,
childFactory = ::createChild
)
init {
logScoped()
}
private fun logScoped() {
Logger.i("koinScopes") {
"""
ParentComponent
ParentScoped: ${getScoped<ParentScoped>()}"
ChildScoped: ${getScoped<ChildScoped>()}
""".trimIndent()
}
}
// just a helper function for logs; don't use this in prod;
private inline fun <reified T : Any> getScoped() = try {
get<T>()
} catch (e: Exception) {
e.message
}
private fun createChild(
config: Config,
componentContext: AppComponentContext
): Child {
return when (config) {
Config.Child -> Child.MyChild(DefaultChildComponent(componentContext.withKoin<ChildComponent>()))
}
}
override fun selectTab(screen: Config) {
navigation.pushToFront(screen)
}
}
package ru.pberdnik.example.features.home.navigation
import co.touchlab.kermit.Logger
import org.koin.core.component.get
import org.koin.core.component.inject
import ru.pberdnik.example.core.di_extension.AppComponentContext
import ru.pberdnik.example.features.di.ChildScoped
import ru.pberdnik.example.features.di.ParentScoped
interface ChildComponent
class DefaultChildComponent(
componentContext: AppComponentContext
) : ChildComponent, AppComponentContext by componentContext {
private val parentScoped: ParentScoped by inject()
init {
logScoped()
}
private fun logScoped() {
Logger.i("koinScopes") {
"""
ChildComponent
ParentScoped: $parentScoped
ParentScoped: ${getScoped<ParentScoped>()}"
ChildScoped: ${getScoped<ChildScoped>()}
""".trimIndent()
}
}
// just a helper function for logs; don't use this in prod;
private inline fun <reified T : Any> getScoped() = try {
get<T>()
} catch (e: Exception) {
e.message
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment