docs/reference/koin-compose/compose-lifecycle.md
This guide covers how Koin integrates with Compose's lifecycle and state management. Understanding these concepts helps you write efficient, bug-free Compose applications.
:::info This guide aligns with Android's official Compose lifecycle documentation. :::
A Composable has three lifecycle events:
Koin's Compose APIs are designed to work efficiently with this lifecycle.
koinInject() retrieves instances from Koin and remembers them across recompositions:
@Composable
fun MyScreen() {
// Resolved once, remembered across recompositions
val repository = koinInject<UserRepository>()
// Safe - uses the same instance
val users by repository.users.collectAsState()
}
Inject dependencies at the Composable function level, not inside callbacks:
@Composable
fun MyScreen() {
// Correct - resolved at composition time
val repository = koinInject<UserRepository>()
val viewModel = koinViewModel<MyViewModel>()
Button(onClick = {
// Wrong - don't inject in callbacks
val service = koinInject<Service>() // Avoid!
// Correct - use already-injected instance
repository.save()
}) {
Text("Save")
}
}
When using parameters with koinInject, prefer the explicit parameter form:
@Composable
fun MyScreen(userId: String) {
// More efficient - parameters evaluated once
val presenter = koinInject<UserPresenter>(
parameters = parametersOf(userId)
)
// Less efficient - lambda re-evaluated on recomposition
val presenter = koinInject<UserPresenter> {
parametersOf(userId)
}
}
The standard pattern for reactive UI with Koin:
@KoinViewModel
class UserViewModel(
private val repository: UserRepository
) : ViewModel() {
private val _state = MutableStateFlow<UiState>(UiState.Loading)
val state: StateFlow<UiState> = _state.asStateFlow()
init {
loadUsers()
}
private fun loadUsers() {
viewModelScope.launch {
_state.value = UiState.Success(repository.getUsers())
}
}
}
@Composable
fun UserScreen(
viewModel: UserViewModel = koinViewModel()
) {
val state by viewModel.state.collectAsState()
when (val s = state) {
is UiState.Loading -> LoadingIndicator()
is UiState.Success -> UserList(s.users)
is UiState.Error -> ErrorMessage(s.message)
}
}
For simpler cases, inject repositories directly:
@Singleton
class UserRepository {
private val _users = MutableStateFlow<List<User>>(emptyList())
val users: StateFlow<List<User>> = _users.asStateFlow()
}
@Composable
fun UserListScreen() {
val repository = koinInject<UserRepository>()
val users by repository.users.collectAsState()
LazyColumn {
items(users) { user ->
UserCard(user)
}
}
}
Use the right tool for each job:
@Composable
fun MyScreen() {
// Koin-managed dependencies
val viewModel = koinViewModel<MyViewModel>()
val repository = koinInject<Repository>()
// Compose-managed state
val scrollState = rememberScrollState()
val coroutineScope = rememberCoroutineScope()
var text by remember { mutableStateOf("") }
// Don't wrap koinInject in remember (unnecessary)
val service = remember { koinInject<Service>() } // Redundant!
}
Execute suspending code when composition enters or keys change:
@Composable
fun UserDetailScreen(userId: String) {
val repository = koinInject<UserRepository>()
var user by remember { mutableStateOf<User?>(null) }
// Runs when userId changes
LaunchedEffect(userId) {
user = repository.getUser(userId)
}
user?.let { UserContent(it) }
}
Clean up resources when leaving composition:
@Composable
fun EventScreen() {
val eventBus = koinInject<EventBus>()
DisposableEffect(Unit) {
val listener = eventBus.subscribe { event ->
// Handle event
}
onDispose {
eventBus.unsubscribe(listener)
}
}
}
Execute non-suspending side effects after every successful recomposition:
@Composable
fun AnalyticsScreen(screenName: String) {
val analytics = koinInject<Analytics>()
SideEffect {
analytics.logScreenView(screenName)
}
}
Compose can skip recomposition when inputs haven't changed. For this to work, parameter types must be stable:
// Stable - Compose can skip
@Composable
fun UserCard(
name: String, // Primitive - stable
onClick: () -> Unit, // Lambda - stable
viewModel: UserViewModel = koinViewModel() // Treated as stable
)
// Potentially unstable - may not skip
@Composable
fun UserCard(
user: User // Data class - stable if all properties stable
)
Koin injections are treated as stable because they return the same instance (for singletons) or are remembered:
@Composable
fun MyScreen() {
// Stable - singleton returns same instance
val repository = koinInject<UserRepository>()
// Stable - ViewModel is remembered
val viewModel = koinViewModel<MyViewModel>()
}
| Pass as Parameter | Inject with Koin |
|---|---|
| Changes frequently (userId, query) | Stable dependencies (repositories, services) |
| UI state (selected item) | Infrastructure (database, network) |
| Navigation arguments | Business logic (use cases) |
| Parent-provided data | ViewModels |
// userId changes - pass as parameter
// repository is stable - inject
@Composable
fun UserProfile(
userId: String,
repository: UserRepository = koinInject()
) {
var user by remember { mutableStateOf<User?>(null) }
LaunchedEffect(userId) {
user = repository.getUser(userId)
}
user?.let { ProfileContent(it) }
}
// Pure composable - no injection needed
@Composable
fun ProfileContent(user: User) {
Column {
Text(user.name)
Text(user.email)
}
}
@Composable
fun FeatureScreen() {
// Inject here
val viewModel = koinViewModel<FeatureViewModel>()
val repository = koinInject<FeatureRepository>()
// Pass down to children
FeatureContent(
state = viewModel.state,
onAction = viewModel::handleAction
)
}
// Pure - receives all data as parameters
@Composable
fun UserCard(
user: User,
onEdit: () -> Unit,
onDelete: () -> Unit
) {
// No injection here
}
// Complex state management in ViewModel
@KoinViewModel
class SearchViewModel(
private val searchRepository: SearchRepository
) : ViewModel() {
var query by mutableStateOf("")
private set
private val _results = MutableStateFlow<List<Result>>(emptyList())
val results = _results.asStateFlow()
fun updateQuery(newQuery: String) {
query = newQuery
viewModelScope.launch {
_results.value = searchRepository.search(newQuery)
}
}
}
@Composable
fun UserList(userIds: List<String>) {
// Inject once outside the loop
val repository = koinInject<UserRepository>()
LazyColumn {
items(userIds) { userId ->
// Don't inject inside items!
UserCard(userId, repository)
}
}
}