CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-io-insert-koin--koin-compose

Jetpack Compose integration for Koin dependency injection framework providing Compose-specific APIs for dependency injection

Pending
Overview
Eval results
Files

scope-management.mddocs/

Scope Management

Advanced scope management capabilities allowing creation and management of Koin scopes with automatic lifecycle handling tied to Compose composition lifecycle. Scopes are automatically closed when compositions are forgotten or abandoned.

Capabilities

KoinScope with Scope Definition

Create a Koin scope using a lambda definition and automatically manage its lifecycle with Compose.

/**
 * Create Koin Scope & close it when Composition is on onForgotten/onAbandoned
 *
 * @param scopeDefinition - lambda to define scope
 * @param content - composable content that uses the scope
 */
@Composable
@KoinExperimentalAPI
fun KoinScope(
    scopeDefinition: Koin.() -> Scope,
    content: @Composable () -> Unit
)

Usage Examples:

import org.koin.compose.getKoin
import org.koin.compose.scope.KoinScope

@Composable
fun UserProfileScreen(userId: String) {
    KoinScope(
        scopeDefinition = { 
            createScope<UserProfileScope>("user-profile-$userId")
        }
    ) {
        // Content within this scope can access UserProfile-scoped dependencies
        UserProfileContent()
        UserSettingsPanel()
    }
    // Scope is automatically closed when this composition is forgotten
}

@Composable
fun SessionBasedScreen() {
    val sessionId = remember { UUID.randomUUID().toString() }
    
    KoinScope(
        scopeDefinition = { 
            getOrCreateScope("session", StringQualifier(sessionId))
        }
    ) {
        // All composables here share the same session scope
        HeaderComponent()
        MainContent()
        FooterComponent()
    }
}

KoinScope with Typed Scope

Create a Koin scope from a specific type with automatic lifecycle management.

/**
 * Create Koin Scope from type T & close it when Composition is on onForgotten/onAbandoned
 *
 * @param scopeID - unique identifier for the scope
 * @param content - composable content that uses the scope
 */
@Composable
@KoinExperimentalAPI
inline fun <reified T : Any> KoinScope(
    scopeID: ScopeID,
    noinline content: @Composable () -> Unit
)

Usage Examples:

import org.koin.compose.scope.KoinScope

// Define scope types
class UserScope
class FeatureScope  
class ActivityScope

@Composable
fun UserDashboard(userId: String) {
    KoinScope<UserScope>(
        scopeID = "user-$userId"
    ) {
        // This content has access to UserScope-scoped dependencies
        UserHeader()
        UserStats()
        UserActions()
    }
}

@Composable
fun FeatureScreen(featureId: String) {
    KoinScope<FeatureScope>(
        scopeID = "feature-$featureId"
    ) {
        FeatureContent()
    }
}

KoinScope with Qualifier

Create a Koin scope with both scope ID and qualifier for more specific scope management.

/**
 * Create Koin Scope from type with qualifier & close it when Composition is on onForgotten/onAbandoned
 *
 * @param scopeID - unique identifier for the scope
 * @param scopeQualifier - qualifier for the scope
 * @param content - composable content that uses the scope
 */
@Composable
@KoinExperimentalAPI
fun KoinScope(
    scopeID: ScopeID,
    scopeQualifier: Qualifier,
    noinline content: @Composable () -> Unit
)

Usage Examples:

import org.koin.compose.scope.KoinScope
import org.koin.core.qualifier.named

@Composable
fun MultiTenantScreen(tenantId: String, environment: String) {
    KoinScope(
        scopeID = "tenant-$tenantId",
        scopeQualifier = named(environment) // "dev", "staging", "prod"
    ) {
        // Content uses tenant-specific, environment-qualified scope
        TenantHeader()
        TenantContent()
    }
}

@Composable  
fun GameSession(gameId: String, difficulty: String) {
    KoinScope(
        scopeID = "game-$gameId",
        scopeQualifier = named("difficulty-$difficulty")
    ) {
        GameBoard()
        GameStats()
        GameControls()
    }
}

rememberKoinScope

Remember a Koin scope and handle its lifecycle with Compose's remember system.

/**
 * Remember Koin Scope & run CompositionKoinScopeLoader to handle scope closure
 *
 * @param scope - Koin scope to remember and manage
 * @return The managed scope instance
 */
@Composable
@KoinExperimentalAPI
fun rememberKoinScope(scope: Scope): Scope

Usage Examples:

import org.koin.compose.getKoin  
import org.koin.compose.scope.rememberKoinScope

@Composable
fun CustomScopeManagement(scopeId: String) {
    val koin = getKoin()
    
    // Create scope manually
    val customScope = remember(scopeId) {
        koin.createScope<MyCustomScope>(scopeId)
    }
    
    // Remember and manage its lifecycle
    val managedScope = rememberKoinScope(customScope)
    
    // Use the managed scope
    val scopedService = remember(managedScope) {
        managedScope.get<MyScopedService>()
    }
    
    MyCustomContent(scopedService)
}

@Composable
fun ConditionalScope(shouldCreateScope: Boolean) {
    if (shouldCreateScope) {
        val scope = remember { 
            getKoin().createScope<ConditionalScope>("conditional")
        }
        
        val managedScope = rememberKoinScope(scope)
        
        ConditionalContent(managedScope)
    } else {
        DefaultContent()
    }
}

Lifecycle Management

Automatic Scope Closure

All KoinScope composables automatically close their scopes when:

  • onForgotten: Composition is forgotten (e.g., navigating away)
  • onAbandoned: Composition is abandoned (e.g., configuration change)
// Internal lifecycle management
class CompositionKoinScopeLoader(val scope: Scope) : RememberObserver {
    override fun onRemembered() {
        // Scope is active
    }
    
    override fun onForgotten() {
        close()
    }
    
    override fun onAbandoned() {
        close()
    }
    
    private fun close() {
        if (!scope.isRoot && !scope.closed) {
            scope.close()
        }
    }
}

Manual Scope Management

@Composable
fun ManualScopeLifecycle() {
    val scope = remember { getKoin().createScope<MyScope>("manual") }
    
    // Manual lifecycle control
    DisposableEffect(scope) {
        onDispose {
            if (!scope.closed) {
                scope.close()
            }
        }
    }
    
    // Don't use rememberKoinScope if managing manually
    ScopeContent(scope)
}

Scope Definition Patterns

User-Based Scopes

@Composable
fun UserScopedScreen(user: User) {
    KoinScope<UserScope>(
        scopeID = "user-${user.id}"
    ) {
        // All user-related components share this scope
        UserProfile(user)
        UserPreferences(user)
        UserActivity(user)
    }
}

Feature-Based Scopes

@Composable
fun FeatureModule(featureFlag: String) {
    KoinScope(
        scopeDefinition = { 
            createScope<FeatureScope>("feature-$featureFlag")
        }
    ) {
        if (featureFlag == "new_ui") {
            NewUIComponents()
        } else {
            LegacyUIComponents()
        }
    }
}

Navigation-Based Scopes

@Composable
fun NavigationScope(route: String) {
    KoinScope(
        scopeID = "nav-$route",
        scopeQualifier = named("navigation")
    ) {
        when (route) {
            "home" -> HomeScreen()
            "profile" -> ProfileScreen()
            "settings" -> SettingsScreen()
        }
    }
}

Scope Injection Within Scoped Content

When inside a KoinScope, use koinInject normally - it will automatically use the current scope:

@Composable
fun UserProfileScreen(userId: String) {
    KoinScope<UserScope>(scopeID = "user-$userId") {
        UserProfileContent() // Can inject UserScope dependencies
    }
}

@Composable
fun UserProfileContent() {
    // This will inject from the UserScope
    val userRepository: UserRepository = koinInject()
    val userPreferences: UserPreferences = koinInject()
    
    // Content using scoped dependencies
    Text("User: ${userRepository.getCurrentUser().name}")
}

Error Handling

Scope Creation Failures

@Composable
fun SafeScopeCreation(scopeId: String) {
    try {
        KoinScope<MyScope>(scopeID = scopeId) {
            ScopeContent()
        }
    } catch (e: ScopeAlreadyCreatedException) {
        // Handle duplicate scope creation
        Text("Scope already exists: $scopeId")
    } catch (e: Exception) {
        // Handle other scope creation errors
        Text("Failed to create scope: ${e.message}")
    }
}

Closed Scope Access

@Composable
fun ScopeAwareContent() {
    val scope = currentKoinScope()
    
    if (scope.closed) {
        Text("Scope is closed")
    } else {
        try {
            val service: MyService = koinInject()
            ServiceContent(service)
        } catch (e: ClosedScopeException) {
            Text("Scope was closed during access")
        }
    }
}

Core Types

// Scope types
typealias ScopeID = String

interface Scope {
    val id: String
    val isRoot: Boolean
    val closed: Boolean
    val logger: Logger
    fun close()
}

// Scope management
interface RememberObserver {
    fun onRemembered()
    fun onForgotten()
    fun onAbandoned()
}

// Qualifiers for scope identification
interface Qualifier
class StringQualifier(val value: String) : Qualifier
fun named(name: String): Qualifier

Best Practices

Scope Naming

// ✅ Good: Descriptive, unique scope IDs
KoinScope<UserScope>("user-${user.id}")
KoinScope<FeatureScope>("feature-shopping-cart")  
KoinScope<SessionScope>("session-${sessionId}")

// ❌ Poor: Generic, collision-prone scope IDs  
KoinScope<UserScope>("scope")
KoinScope<FeatureScope>("temp")

Scope Granularity

// ✅ Good: Appropriate scope granularity
@Composable
fun ShoppingFlow() {
    KoinScope<ShoppingScope>("shopping-session") {
        ProductCatalog()
        ShoppingCart() 
        Checkout()
    }
}

// ❌ Poor: Too many nested scopes
@Composable  
fun OverScopedFlow() {
    KoinScope<Scope1>("scope1") {
        KoinScope<Scope2>("scope2") {
            KoinScope<Scope3>("scope3") {
                Content() // Too much nesting
            }
        }
    }
}

Scope Lifecycle Alignment

// ✅ Good: Scope lifetime matches business logic
@Composable
fun UserSession(user: User) {
    // Scope lives for entire user session
    KoinScope<UserScope>("user-${user.id}") {
        UserDashboard()
    }
}

// ✅ Good: Scope per business operation
@Composable
fun CheckoutFlow(cartId: String) {
    // Scope lives for checkout process
    KoinScope<CheckoutScope>("checkout-$cartId") {
        CheckoutSteps()
    }
}

Install with Tessl CLI

npx tessl i tessl/maven-io-insert-koin--koin-compose

docs

application-setup.md

context-access.md

dependency-injection.md

index.md

module-management.md

scope-management.md

tile.json