Explains why each piece exists and how to wire them together.


UiEvent – Capturing User Actions

Purpose: 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 Truth

Purpose: holds everything the composable needs to render.

<aside> 💡

@Immutable
data class ProfileUiState(
    override val eventSink: (ProfileUiEvent) -> Unit,
) : UiState<ProfileUiEvent>

ViewModel – UI logic

Purpose: 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 Layer

Purpose: 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