The Screen: ```@Composable fun LoginScreen( /*...
# orbit-mvi
s
The Screen:
Copy code
@Composable
fun LoginScreen(
    /** States **/
    viewModel: LoginViewModel = viewModel(),
    onSuccess: (LoginResult) -> Unit,
    onNavigateToRegistration: () -> Unit,
) {
    val context = LocalContext.current
    viewModel.collectSideEffect {
        when (it) {
            is LoginScreenEffect.LoginSuccess -> onSuccess.invoke(LoginResult())
            is LoginScreenEffect.ShowToastError -> Toast.makeText(
                context,
                it.message,
                Toast.LENGTH_SHORT
            ).show()
        }
    }

    Box {
        Column(modifier = Modifier.fillMaxSize()) {
            Text(text = "Login Screen")
            val state by viewModel.collectAsState()
            val showLoading by remember(state.showLoginLoading) { derivedStateOf { state.showLoginLoading } }
            if (showLoading)
                CircularProgressIndicator()

            Spacer(modifier = Modifier.weight(1f))

            val emailError by remember(state.emailError) { derivedStateOf { state.emailError } }
            val passwordError by remember(state.passwordError) { derivedStateOf { state.passwordError } }
            LoginForm(
                emailError = emailError,
                passwordError = passwordError,
                onEmailInput = { viewModel.updateEmail(it) },
                onPasswordInput = { viewModel.updatePassword(it) }
            )

            val enableButton by remember(state) { derivedStateOf { state.enableSubmitButton } }
            Button(
                onClick = { viewModel.submitLogin() },
                enabled = enableButton
            ) {
                Text(text = "Login")
            }

            Spacer(modifier = Modifier.height(24.dp))

            Button(
                onClick = { onNavigateToRegistration.invoke() },
                colors = ButtonDefaults.filledTonalButtonColors()
            ) {
                Text(text = "Go To Registration")
            }

            Spacer(modifier = Modifier.weight(1f))
        }
    }
}

@Composable
fun LoginForm(
    emailError: String,
    passwordError: String,
    onEmailInput: (String) -> Unit,
    onPasswordInput: (String) -> Unit,
) {
    Column {
        var emailBuffer by remember { mutableStateOf("") }
        TextField(
            value = emailBuffer,
            onValueChange = {
                emailBuffer = it
                onEmailInput.invoke(it)
            },
            isError = emailError.isNotBlank(),
        )
        if (emailError.isNotBlank())
            Text(text = emailError, color = MaterialTheme.colorScheme.error)

        var passwordBuffer by remember { mutableStateOf("") }
        TextField(
            value = passwordBuffer,
            onValueChange = {
                passwordBuffer = it
                onPasswordInput.invoke(it)
            },
            isError = passwordError.isNotBlank()
        )
        if (passwordError.isNotBlank())
            Text(text = passwordError, color = MaterialTheme.colorScheme.error)
    }
}
The State:
Copy code
@Immutable
@optics
data class LoginScreenSate(
    val email: String = "",
    val password: String = "",
    val submitLoginDataState: VmState<LoginCredential> = VmIdle()
) {
    companion object

    val showLoginLoading: Boolean = submitLoginDataState is VmProcessing
    val emailError: String
        get() = when {
            !email.matches(EmailPattern.toRegex()) -> "Format Salah"
            else -> ""
        }
    val passwordError: String = when {
        password.length < 10 -> "Pasword kurang panjang"
        else -> ""
    }
    val enableSubmitButton =
        emailError.isBlank() && passwordError.isBlank() && submitLoginDataState.fold(ifProcessing = { false }) { true }
}
The ViewModel
Copy code
class LoginViewModel : ContainerHost<LoginScreenSate, LoginScreenEffect>, ViewModel() {

    override val container: Container<LoginScreenSate, LoginScreenEffect> =
        container(LoginScreenSate())

    fun updateEmail(email: String) = intent {
        reduce {
            val lens = LoginScreenSate.email::set
            lens(state, email)
        }
    }

    fun updatePassword(password: String) = intent {
        reduce {
            val lens = LoginScreenSate.password
            lens.set(state, password)
        }
    }

    fun submitLogin() = intent {
        val lens = LoginScreenSate.submitLoginDataState

        reduce { lens.set(state, VmProcessing()) }

        // do login stuff
        val loginResultState = runBlocking {
            delay(3000)
            VmSuccess(LoginCredential())
        }

        // assume success
        reduce { lens.set(state, loginResultState) }

        postSideEffect(LoginScreenEffect.LoginSuccess(loginResultState.data))
    }
}