Quick Start — KMP
This walkthrough builds a simple login feature end-to-end using Arkitekt with Decompose and Koin in a Kotlin Multiplatform project.
Define the State
State is a plain data class in common code.
data class LoginState(
val name: String = "",
val surname: String = "",
val fullName: String = "",
val isLoading: Boolean = false,
)
Define Navigation Actions
Navigation actions are declared as an interface extending NavigationActions. The parent component provides the implementation.
interface LoginNavigation : NavigationActions {
fun toHome()
}
Define UI Events
One-shot events are modeled as a sealed interface extending UiEvent.
sealed interface LoginUiEvent : UiEvent {
data class ShowError(val message: String) : LoginUiEvent
}
Build the Component
The component extends BaseComponent<LoginState, LoginUiEvent> and is annotated with @GenerateFactory for automatic factory generation.
To execute use cases, the component implements CoroutineScopeOwner and provides componentCoroutineScope as the coroutine scope.
@GenerateFactory
class LoginComponent(
@InjectedParam componentContext: AppComponentContext,
@InjectedParam navigation: LoginNavigation,
private val loginUseCase: LoginUseCase,
) : BaseComponent<LoginState, LoginUiEvent>(componentContext, LoginState()),
CoroutineScopeOwner {
override val coroutineScope = componentCoroutineScope
val state: StateFlow<LoginState> = componentState
fun onNameChanged(name: String) {
update(componentState) { copy(name = name) }
}
fun onSurnameChanged(surname: String) {
update(componentState) { copy(surname = surname) }
}
fun logIn() {
val currentState = componentState.value
val args = LoginData(
name = currentState.name,
surname = currentState.surname,
)
loginUseCase.execute(args) {
onStart {
update(componentState) { copy(isLoading = true) }
}
onSuccess {
update(componentState) {
copy(
isLoading = false,
fullName = "${args.name} ${args.surname}",
)
}
}
onError { error ->
update(componentState) { copy(isLoading = false) }
sendUiEvent(LoginUiEvent.ShowError(error.message.orEmpty()))
}
}
}
}
Compose the Screen (Android)
On the Android target, the screen composable receives the component, collects its state, and uses EventsEffect from the decompose module to handle one-shot events.
@Composable
fun LoginScreen(component: LoginComponent) {
val state by component.state.collectAsState()
Column {
TextField(
value = state.name,
onValueChange = { component.onNameChanged(it) },
label = { Text("Name") },
)
TextField(
value = state.surname,
onValueChange = { component.onSurnameChanged(it) },
label = { Text("Surname") },
)
Button(
onClick = { component.logIn() },
enabled = !state.isLoading,
) {
Text("Log In")
}
if (state.fullName.isNotEmpty()) {
Text(text = "Welcome, ${state.fullName}!")
}
}
EventsEffect(component.events) {
onEvent<LoginUiEvent.ShowError> { event ->
// Show error snackbar or toast
}
}
}
Koin Module Setup
Register the use case in a Koin module so the generated LoginComponentFactory can resolve it automatically.
val loginModule = module {
factory { LoginUseCase(get()) }
}
The @GenerateFactory annotation generates a LoginComponentFactory that resolves all non-@InjectedParam constructor parameters from Koin. You only supply componentContext and navigation manually when creating the component.