Last active
April 16, 2025 08:20
-
-
Save pberdnik/3d052989f5299a78378a640d1920b281 to your computer and use it in GitHub Desktop.
Koin + Decompose
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>() | |
) | |
) | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// gist name |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment