Explains why each piece exists and how to wire them together.
UiEvent
– Capturing User ActionsPurpose: enumerate every user interaction (clicks, swipes, etc.).
<aside> 💡
Start minimal; add events as the UI grows.
</aside>
sealed interface ProfileUiEvent : UiEvent {
data object PerformLogout : ProfileUiEvent
}
UiState
– UI State Single Source of TruthPurpose: holds everything the composable needs to render.
<aside> 💡
@Immutable
data class ProfileUiState(
override val eventSink: (ProfileUiEvent) -> Unit,
) : UiState<ProfileUiEvent>
ViewModel
– UI logicPurpose: business logic & state producer (BaselineViewModel).
<aside> 💡
state()
is @Composable to leverage Compose snapshots and make sure UI is updated automatically, when the state changes.
</aside>
@Inject
class ProfileViewModel : BaselineViewModel<ProfileUiEvent, ProfileUiState>() {
private val sectionsFlow = mutableState(persistentListOf()) { createSections() }
@Composable
override fun state(): ProfileUiState {
val sections by sectionsFlow.collectAsStateWithLifecycle()
return ProfileUiState(sections) { event ->
when (event) {
ProfileUiEvent.PerformLogout -> handleLogout()
}
}
}
private fun handleLogout() { /* … */ }
private suspend fun createSections(): ImmutableList<Section> { /* … */ }
}
Screen
– Pure UI LayerPurpose: stateless composable that renders the layout.
<aside> 💡
Forward interactions via lambdas only.
</aside>
@Composable
fun ProfileScreen(
sections: ImmutableList<Section>,
onLogoutClicked: () -> Unit,
) {
/* UI layout */
}
Route
– Glue Layer