Setup app with scopes
Please read the scopes documentation first to grasp the 3 scopes being used in the IDK.
1. Initialize application component and scope
Typically, there only is one application scope within a single app or service. Technically, multiple instances could be created. The app scope has an app component, that is initialized with some information. The app component acts as a singleton in the app scope.
The application parameter below for instance is the typically the Android context or Apple app object.
The appId is the name of your application. It is mainly being used in configuration management. All configuration properties being used, will use the appId internally.
Same for the profile. Within a single app you can support multiple profiles. However, a single app instance will only be started with a single profile. What a profile exactly is,
is up to you as a developer. A typical use case is the type of environment the app is running in. The profile is also being used in the configuration service to distinguish, for
instance, between different configuration values between your production and test app. Lastly there is the app version. Although the component expects your app version to be
supplied there, currently it is not really in use yet in the IDK.
// Create your application component
val appComponent = YourAppComponent.init(
application = yourApplication, // This can be your mobile app reference instance
appId = "your-app-id", // A string denoting your app
profile = "prod", // A profile so you could have different properties for different environments or profiles of the app
version = "1.0.0" // The version of your app.
)
Now you get access to a few objects in the app scope via the appComponent above. The app component however knows about quite a lot more interfaces it can injects. Its primary
function is to ensure that any objects being created with these interfaces are using Dependency Injection, so a developer does not directly provide an actual implementation. This
allows for clean, cohesive code with low coupling. One of the most important objects on the app component is the contextManager which we will use next.
2. Initialize context scope:
Once the application component has been created it is time to create the context scope, which contains the tenant and principal values. A principal either is a natural person (user) or system account. This scope is used to allow separation of object instances and asynchronous code. It is also being used by the configuration system, meaning that it is possible to have different configuration values for different tenants, or even principals. If multi-tenancy is not needed in your use case, you can either always initialize with a fake tenant and principal, or simply leverage the anonymous context scope.
// Initialize context with tenant and principal information
val contextComponent = appComponent.contextManager
.initFromTenantAndPrincipal(
TenantInputString("your-tenant-id"),
PrincipalInputString("your-principal-id@for-instance-an-email.com")
)
// As an anonymous user, or if you don't use multi-tenancy at all:
//val contextComponent = appComponent.contextManager.initAnonymous()
3. Initialize Session scope:
Now we have a singleton app scope, and a context scope for the tenant and principal, it is time to create a session. You can define what a session entails. It could be a long-lived session (from user login to logout in a mobile app) or a short-lived one (a single request/response in a REST API, for instance). The session scope is mainly used to bind all objects and asynchronous code together. This means that any outstanding work within a session will be stopped once the session is destroyed.
// Initialize session
val sessionContextManager = contextComponent.sessionContextManager
val sessionComponent = sessionContextManager.initSession("unique-example-session") // A unique value for the session
Create your components
Since we are using Dependency Injection and have gradle module seperation between interfaces and implementations, it is possible to swap out implementations of the interfaces
without having to change other code that relies on the interfaces. The public modules contain the interfaces and data classes, whilst the impl modules contain
the actual implementations.
The DI system brings all the contributions together at build time. All components, subcomponents and injections are being brought together and this should typically happen in your app. If you do not have a Kotlin Multiplatform App, we suggest to create a specific project in Kotlin Multiplatform with the specific purpose to build the modules and dependencies. The 3 scope components then can be used in your app, and will only contain the actual implementations you need.
Example components
Below you can find example code to create components that you could use in your app.
- App scope
- Context scope
- Session scope
- Imports
/**
* Jvm implementation for [AbstractAppComponent] and provides the package name as application
* ID as well as the application and its context.
*
* This class is a singleton and automatically provided in the dependency graph whenever you
* inject [AbstractAppComponent] through the [ContributesBinding] annotation.
*/
@SingleIn(AppScope::class)
@MergeComponent(AppScope::class)
@Component
abstract class YourAppComponent(
application: Any,
appId: String,
profile: String,
version: String,
) : YourAppComponentMerged, AbstractAppComponent(application, version, appId, profile, DefaultSureRootScopeProvider()) {
companion object {
fun init(
application: Any,
appId: String,
profile: String,
version: String,
): TestAppComponent {
val component = TestAppComponent::class.create(
application,
appId,
profile,
version
)
component.initRootScopeProvider()
return component
}
}
}
@SingleIn(UserContextScope::class)
@MergeComponent(UserContextScope::class)
@Component
abstract class YourContextComponent(
@Component
val appComponent: YourAppComponent // <-- The app component is being injected here.
) : YourContextComponentMerged
@MergeComponent(SessionScope::class)
@SingleIn(SessionScope::class)
@Component
abstract class YourSessionComponent(
@Component
val appComponent: YourAppComponent, // <-- The app component is being injected here.
@Component
val contextComponent: YourContextComponent // <-- The context component is being injected here.>
) : YourSessionComponentMerged
package my.example
import me.tatarka.inject.annotations.Component
import software.amazon.lastmile.kotlin.inject.anvil.AppScope
import software.amazon.lastmile.kotlin.inject.anvil.ContributesBinding
import software.amazon.lastmile.kotlin.inject.anvil.MergeComponent
import software.amazon.lastmile.kotlin.inject.anvil.SingleIn
import sure.di.app.AbstractAppComponent
import sure.core.defaults.app.DefaultSureRootScopeProvider
import sure.di.context.UserContextScope
import sure.di.session.SessionScope
The interfaces being extended ending with Merged are being used to merge the components together. This is done by the MergeComponent annotation and KSP.
These files and interfaces are automatically generated at build time. Please make sure that the name before the Merged part is the same as the name of the component.
It is important to have components for all 3 scopes, just at is is important to inject the components of lower scopes into higher scopes. For example the AppScope has no additional components injects, but the Context scope has the app scope component injected, just like the session scope has both the app and context scope components injected. This ensures that the entire dependency graph is available in the respective scopes. So any injection at the app scope level will also be available at the context scope level, and so on.
It makes sense to have the above code somewhere early in your application. For instance during app start, or close to the authentication process.
Note You could be using kotlin-inject in your own code as well, in which case you could inject an IKiwaHolderServices property as constructor argument. You would automatically be provided with an instance.
Kotlin Symbol Processing
We are using KSP to generate the components and interfaces. Kotlin-inject, kotlin-inject-anvil and Amazon App platform provide contributions to KSP to make code generation happen at build time. The downside is a slightly longer build time. The upside is that DI happens at compile time instead of runtime which means:
- Errors visible during build time
- No runtime overhead
- No reflection
- No runtime dependencies
In order to use KSP with injection you need to setup any project that needs to generate code properly
- KSP
- Toml catalog
ksp {
// [ksp] Cannot find an @Inject constructor or provider for: kotlinx.coroutines.CoroutineDispatcher
//provideAppScopeCoroutineScopeScoped(dispatcher: kotlinx.coroutines.CoroutineDispatcher): software.amazon.app.platform.scope.coroutine.CoroutineScopeScoped
//appScopeCoroutineScopeScoped: software.amazon.app.platform.scope.coroutine.CoroutineScopeScoped
// Only set to false if you get the above error about injecting CoroutineDispatcher
useKsp2.set(false)
// We are using the Amazon App Platform binding processor instead of the Kotlin Inject anvil binding processor!
arg("software.amazon.lastmile.kotlin.inject.anvil.processor.ContributesBindingProcessor", "disabled")
}
dependencies {
// Adjust for the languages you need
addProvider("kspJs", libs.kotlin.inject.compiler.ksp)
add("kspJs", libs.amz.kotlin.inject.contribute.public)
add("kspJs", libs.amz.kotlin.inject.contribute.code.generators)
add("kspJs", libs.anvil.compiler.ksp)
addProvider("kspJsTest", libs.kotlin.inject.compiler.ksp)
add("kspJsTest", libs.amz.kotlin.inject.contribute.public)
add("kspJsTest", libs.amz.kotlin.inject.contribute.code.generators)
add("kspJsTest", libs.anvil.compiler.ksp)
addProvider("kspJvm", libs.kotlin.inject.compiler.ksp)
add("kspJvm", libs.amz.kotlin.inject.contribute.public)
add("kspJvm", libs.amz.kotlin.inject.contribute.code.generators)
add("kspJvm", libs.anvil.compiler.ksp)
addProvider("kspJvmTest", libs.kotlin.inject.compiler.ksp)
add("kspJvmTest", libs.amz.kotlin.inject.contribute.public)
add("kspJvmTest", libs.amz.kotlin.inject.contribute.code.generators)
add("kspJvmTest", libs.anvil.compiler.ksp)
addProvider("kspIosX64", libs.kotlin.inject.compiler.ksp)
add("kspIosX64", libs.amz.kotlin.inject.contribute.public)
add("kspIosX64", libs.amz.kotlin.inject.contribute.code.generators)
add("kspIosX64", libs.anvil.compiler.ksp)
addProvider("kspIosArm64", libs.kotlin.inject.compiler.ksp)
add("kspIosArm64", libs.amz.kotlin.inject.contribute.public)
add("kspIosArm64", libs.amz.kotlin.inject.contribute.code.generators)
add("kspIosArm64", libs.anvil.compiler.ksp)
addProvider("kspLinuxX64", libs.kotlin.inject.compiler.ksp)
add("kspLinuxX64", libs.amz.kotlin.inject.contribute.public)
add("kspLinuxX64", libs.amz.kotlin.inject.contribute.code.generators)
add("kspLinuxX64", libs.anvil.compiler.ksp)
}