Skip to main content
Version: v0.25.0 (Latest)

Application Setup

Set up an IDK application: define the application graph, initialize it at startup, create user contexts and sessions, and access services.

Defining Your Application Graph

Every application that uses the IDK defines its own application graph. This is an abstract class annotated with @DependencyGraph(AppScope::class) that extends AbstractAppGraph. At compile time, Metro merges all contributed bindings from the IDK modules on your classpath into this graph, giving you a single entry point to the full scope hierarchy.

import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.DependencyGraph
import dev.zacsweers.metro.Named
import dev.zacsweers.metro.Provides
import dev.zacsweers.metro.createGraphFactory
import com.sphereon.core.defaults.app.DefaultRootScopeProvider
import com.sphereon.di.app.AbstractAppGraph
import com.sphereon.di.app.RootScopeProvider

@DependencyGraph(AppScope::class)
abstract class MyAppGraph : AbstractAppGraph() {

@DependencyGraph.Factory
fun interface Factory {
fun create(
@Provides application: Any,
@Provides @Named("appId") appId: String,
@Provides @Named("profile") profile: String,
@Provides @Named("version") version: String,
@Provides rootScopeProvider: RootScopeProvider,
): MyAppGraph
}

companion object {
fun init(
application: Any,
appId: String,
profile: String,
version: String
): MyAppGraph {
val graph = createGraphFactory<MyAppGraph.Factory>().create(
application = application,
appId = appId,
profile = profile,
version = version,
rootScopeProvider = DefaultRootScopeProvider()
)
graph.initRootScopeProvider()
return graph
}
}
}

Because your graph class has @DependencyGraph(AppScope::class), Metro collects every @ContributesTo(AppScope::class) and @ContributesBinding(AppScope::class) declaration from the IDK libraries (and any of your own modules) and wires them into the generated implementation. The child scopes (UserScope and SessionScope) are created automatically as you traverse the scope hierarchy.

See Extending the DI Graph for details on Metro annotations and how to contribute your own services.

Initializing at Startup

Create the application graph once when your application starts. It lives for the entire application lifetime.

class MyApplication : Application() {
lateinit var appGraph: MyAppGraph
private set

override fun onCreate() {
super.onCreate()

appGraph = MyAppGraph.init(
application = this,
appId = "com.example.myapp",
profile = BuildConfig.BUILD_TYPE, // "debug" or "release"
version = BuildConfig.VERSION_NAME
)
}
}

Register your Application class in AndroidManifest.xml:

<application
android:name=".MyApplication"
...>
</application>

Initialization Parameters

ParameterWhat it does
applicationPlatform application instance (Android Context or iOS UIApplication)
appIdUsed in configuration property resolution and logging
profileSelects configuration values (e.g., "debug", "production")
versionIncluded in logs and diagnostics

Setting Up User Context

After creating the application graph, establish a user context. The user context represents a tenant and principal, enabling multi-tenant isolation.

Multi-Tenant Setup

For applications that serve multiple organizations or users:

// Get the user context manager from the app graph
val userContextManager = appGraph.userContextManager

// Create a user context for a specific tenant and user
val userContext = userContextManager.createOrGetFromInputs(
tenantInput = DefaultTenantInputString(tenant = "acme-corp"),
principalInput = DefaultPrincipalInputString(principal = "alice@acme-corp.com"),
makeActive = true
)

Single-Tenant Setup

For applications that don't need multi-tenancy, use the anonymous context:

val userContext = appGraph.userContextManager.getAnonymous(makeActive = true)

The anonymous context provides a default tenant and principal, simplifying the code while maintaining the same scope structure.

Creating Sessions

Sessions are where most work happens. A session represents an active working context with access to services.

// Create a session with a unique identifier
val sessionContextManager = userContext.sessionContextManager
val session = sessionContextManager.createOrGetFromId(
sessionId = "main-session",
makeActive = true
)

// Access the session graph for services
val sessionGraph = session.graph

Session Lifecycle Strategies

Different applications have different session lifecycle needs:

For mobile wallet applications, you might create a long-lived session at user login and destroy it at logout:

// On login
fun onUserLogin(userId: String) {
val userContext = appGraph.userContextManager.createOrGetFromInputs(
tenantInput = DefaultTenantInputString(tenant = "default"),
principalInput = DefaultPrincipalInputString(principal = userId),
makeActive = true
)
session = userContext.sessionContextManager.createOrGetFromId(
sessionId = "user-session",
makeActive = true
)
}

// On logout
fun onUserLogout() {
userContext.sessionContextManager.destroyById(sessionId = "user-session")
session = null
}

For credential presentation flows, you might create a short-lived session for each presentation:

suspend fun presentCredential() {
val session = userContext.sessionContextManager.createOrGetFromId(
sessionId = UUID.randomUUID().toString(),
makeActive = true
)

try {
val engagementManager = session.graph.mdocEngagementManager
// Perform presentation...
} finally {
userContext.sessionContextManager.destroyById(sessionId = session.sessionId)
}
}

Accessing Services

If you contribute your own classes to the DI graph, services are injected as constructor parameters automatically. You can also grab them directly from the session graph:

val sessionGraph = session.graph

val keyManager = sessionGraph.keyManagerService
val engagementManager = sessionGraph.mdocEngagementManager
val oauth2Client = sessionGraph.oauth2Client
val configProvider = sessionGraph.configProvider

Complete Setup Example

Here's a complete example combining all the setup steps:

class MyApplication : Application() {
lateinit var appGraph: MyAppGraph
private set

override fun onCreate() {
super.onCreate()
appGraph = MyAppGraph.init(
application = this,
appId = "com.example.wallet",
profile = BuildConfig.BUILD_TYPE,
version = BuildConfig.VERSION_NAME
)
}
}

class MainActivity : ComponentActivity() {
private lateinit var session: SessionInstance

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// Get the IDK app graph
val appGraph = (application as MyApplication).appGraph

// Create user context (anonymous for this example)
val userContext = appGraph.userContextManager.getAnonymous(makeActive = true)

// Create a session
session = userContext.sessionContextManager.createOrGetFromId(
sessionId = "main-session",
makeActive = true
)

// Now you can use IDK services
val keyManager = session.graph.keyManagerService
val engagementManager = session.graph.mdocEngagementManager

// Continue with your app logic...
}

override fun onDestroy() {
super.onDestroy()
// Clean up session if needed
session.sessionContextManager.destroyById(sessionId = "main-session")
}
}

Next Steps

With your application set up, you can now:

  1. Configure multi-tenancy if your application serves multiple organizations
  2. Set up configuration properties for customizing behavior
  3. Start using IDK services like key management or mDoc presentation