Code

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()) needs TokenStorage for the second get(). If coreModule (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.