Create AuthViewModel.kt — the login screen
In the code column — AuthViewModel.kt. Same MVI shape as the cart (lesson 3): immutable state, one onEvent(), _uiState.update { it.copy(...) }. The trick is that one screen does both log in and sign up: isLoginMode decides which server method to call.
The MVI contract and state:
sealed interface AuthEvent {
data class EmailChanged(val value: String) : AuthEvent
data class PasswordChanged(val value: String) : AuthEvent
data object Submit : AuthEvent // one button; behavior depends on mode
data object ToggleMode : AuthEvent // login ↔ register
}
data class AuthUiState(
val email: String = "", val password: String = "",
val isLoading: Boolean = false, val error: String? = null,
val isLoggedIn: Boolean = false,
val isLoginMode: Boolean = true // true = Log in, false = Sign up
)
LoginScreen — two OutlinedTextFields (email, password with PasswordVisualTransformation), one button (its label depends on the mode), and a LaunchedEffect(state.isLoggedIn) that navigates on success:
LaunchedEffect(state.isLoggedIn) {
if (state.isLoggedIn) onLoginSuccess() // → to the menu
}
Where does the app start? StartupViewModel checks the token at launch and decides the start screen:
sealed interface StartupState { data object Checking; data object LoggedIn; data object LoggedOut }
// init { if (repository.isLoggedIn()) LoggedIn else LoggedOut }
// nav: Checking → splash; LoggedIn → Screen.Menu; LoggedOut → Screen.Login
And Koin (the auth module; a subset — no TableSession, which arrives in lesson 10):
val authModule = module {
single { AuthRepository(get(), get()) } // get() = PicaApi, get() = TokenStorage
viewModel { AuthViewModel(get()) }
viewModel { StartupViewModel(get()) }
}
Verify. Run the backend and the app:
1. First launch → LoggedOut → Login screen.
2. "Sign up", enter email + password → POST /register → get a token → the menu.
3. Close and reopen the app → StartupViewModel finds a valid token → straight to the menu (no login).
Gotcha (Koin).
AuthRepository(get(), get())needsTokenStoragefor the secondget(). IfcoreModule(lesson 6) doesn't register it, the app crashes on the first login:org.koin.core.error.NoBeanDefFoundException: No definition found for type 'TokenStorage'The fix — add to
coreModule:single<TokenStorage> { SecureTokenStorage(get()) }. Always register new dependencies in a module, or Koin has no idea how to build them.