3
\$\begingroup\$

Im working on the login screen for my application. It has fields for users email and password. Also users can click on forgot password button to send an email with new passsword. It can validate these fields, show errors. I would like to hear peoples opinion about how I could improve my code to make it more readible, cleaner and must better. I really want to improve my skills so I would like to get more advices.

LogInEmailViewModel.kt

@HiltViewModel
class LogInEmailViewModel @Inject constructor(
    private val emailValidator: EmailValidator,
    private val passwordValidator: PasswordValidator,
    private val firebaseAuth: FirebaseAuth
) : ViewModel() {

private val _logInEmailScreenState = MutableStateFlow(LogInScreenState())
val logInEmailScreenState = _logInEmailScreenState.asStateFlow()

private val _eventChannel = Channel<Event>()
val eventFlow = _eventChannel.receiveAsFlow()

fun onEvent(logInEvent: LogInEvent) {
    when (logInEvent) {
        is LogInEvent.UpdateEmail -> {
            updateEmail(logInEvent.email)
        }
        is LogInEvent.UpdatePassword -> {
            updatePassword(logInEvent.password)
        }
        LogInEvent.Action -> {
            doAction()
        }
        LogInEvent.ForgotPassword -> {
            goToForgotPasswordScreen()
        }
        LogInEvent.BackPressed -> {
            onBackPressed()
        }
        is LogInEvent.UpdatePasswordVisibility -> {
            changePasswordVisibility(logInEvent.isVisible)
        }
    }
}

private fun updateEmail(newEmail: String) {
    _logInEmailScreenState.value = _logInEmailScreenState.value.copy(email = newEmail, emailError = null)
}

private fun updatePassword(newPassword: String) {
    _logInEmailScreenState.value = _logInEmailScreenState.value.copy(password = newPassword, passwordError = null)
}

private fun doAction() {
    when (_logInEmailScreenState.value.loginState) {
        LoginState.LOGIN -> {
            doLogInAction()
        }
        LoginState.FORGOT_PASSWORD -> {
            doForgotPasswordAction()
        }
    }
}

private fun doLogInAction() {
    val isEmailValid = emailValidator.isValid(_logInEmailScreenState.value.email)
    val isPasswordValid = passwordValidator.isValid(_logInEmailScreenState.value.password)
    when {
        !isEmailValid -> {
            showEmailError()
        }
        !isPasswordValid -> {
            showPasswordError()
        }
        else -> {
            tryToLogIn()
        }
    }
}

private fun tryToLogIn() {
    firebaseAuth.signInWithEmailAndPassword(
        _logInEmailScreenState.value.email,
        _logInEmailScreenState.value.password
    )
        .addOnSuccessListener {
            viewModelScope.launch {
                _eventChannel.send(Event.LogInSuccess)
            }
        }.addOnFailureListener {
            viewModelScope.launch {
                _eventChannel.send(Event.LogInFailed(it.getAuthErrorInfo()))
            }
        }
}

private fun doForgotPasswordAction() {
    val isEmailValid = emailValidator.isValid(_logInEmailScreenState.value.email)
    when {
        !isEmailValid -> {
            showEmailError()
        }
        else -> {
            doSendForgotPasswordEmail()
        }
    }
}

private fun doSendForgotPasswordEmail() {
    firebaseAuth.sendPasswordResetEmail(
        _logInEmailScreenState.value.email
    )
        .addOnSuccessListener {
            goToLogInEmailScreen()
            viewModelScope.launch {
                _eventChannel.send(Event.ForgotPasswordSuccess(getTextContainer(R.string.forgot_password_success)))
            }
        }.addOnFailureListener {
            viewModelScope.launch {
                _eventChannel.send(Event.ForgotPasswordFailed(it.getAuthErrorInfo()))
            }
        }
}

private fun showEmailError() {
    _logInEmailScreenState.value = _logInEmailScreenState.value.copy(
        emailError = getTextContainer(
            R.string.onboarding_login_email_error
        )
    )
}

private fun showPasswordError() {
    _logInEmailScreenState.value = _logInEmailScreenState.value.copy(
        passwordError = getTextContainer(
            R.string.onboarding_login_password_error
        )
    )
}

private fun goToForgotPasswordScreen() {
    _logInEmailScreenState.value = _logInEmailScreenState.value.copy(
        loginState = LoginState.FORGOT_PASSWORD,
        emailError = null,
        passwordError = null
    )
    viewModelScope.launch {
        _eventChannel.send(Event.GoToForgotPasswordScreen)
    }
}

private fun goToLogInEmailScreen() {
    _logInEmailScreenState.value = _logInEmailScreenState.value.copy(
        loginState = LoginState.LOGIN,
        emailError = null,
        passwordError = null
    )
    viewModelScope.launch {
        _eventChannel.send(Event.GoToLogInScreen)
    }
}

private fun onBackPressed() {
    if (_logInEmailScreenState.value.loginState == LoginState.FORGOT_PASSWORD) {
        goToLogInEmailScreen()
    } else {
        viewModelScope.launch {
            _eventChannel.send(Event.GoBack)
        }
    }
}

private fun changePasswordVisibility(isPasswordVisible: Boolean) {
    _logInEmailScreenState.value = _logInEmailScreenState.value.copy(
        isPasswordVisible = isPasswordVisible
    )
}

data class LogInScreenState(
    val loginState: LoginState = LoginState.LOGIN,
    val email: String = "",
    val password: String = "",
    val isPasswordVisible: Boolean = false,
    val emailError: TextContainer? = null,
    val passwordError: TextContainer? = null,
)

enum class LoginState(val actionButtonText: TextContainer) {
    LOGIN(getTextContainer(R.string.onboarding__login_action_button)),
    FORGOT_PASSWORD(getTextContainer(R.string.onboarding__reset_password_action_button))
}

sealed class Event {
    object GoToLogInScreen : Event()
    object GoToForgotPasswordScreen : Event()
    object GoBack : Event()

    object LogInSuccess : Event()
    data class LogInFailed(val errorInfo: TextContainer) : Event()

    data class ForgotPasswordSuccess(val infoMessage: TextContainer) : Event()
    data class ForgotPasswordFailed(val errorInfo: TextContainer) : Event()
  }
}

LogInEmailScreen.kt

private const val FORGOT_PASSWORD_DEFAULT_OFFSET = 0f
private const val FORGOT_PASSWORD_TARGET_ANIMATED_OFFSET = -100f

@OptIn(ExperimentalComposeUiApi::class)
@Composable
@Destination
fun LogInEmailScreen(navigator: DestinationsNavigator, logInEmailViewModel: LogInEmailViewModel = hiltViewModel()) {
    val context = LocalContext.current
    val keyboardController = LocalSoftwareKeyboardController.current
    val lifecycleOwner = LocalLifecycleOwner.current
    val focusManager = LocalFocusManager.current

    val screenState = logInEmailViewModel.logInEmailScreenState.collectAsState().value

    val coroutinesScope = rememberCoroutineScope()
    val passwordVisibilityAnimation = remember { Animatable(VISIBLE) }
    val forgotPasswordAnimationOffsetY = remember { Animatable(0f) }

    LaunchedEffect(Unit) {
        lifecycleOwner.lifecycleScope.launchWhenStarted {
            logInEmailViewModel.eventFlow.collectLatest { event ->
                when (event) {
                    is LogInEmailViewModel.Event.GoToForgotPasswordScreen -> {
                        goToForgotPasswordScreenAnimation(
                            coroutinesScope,
                            passwordVisibilityAnimation,
                            forgotPasswordAnimationOffsetY
                        )
                    }
                    is LogInEmailViewModel.Event.LogInSuccess -> {
                        navigator.navigate(MainScreenDestination())
                    }
                    LogInEmailViewModel.Event.GoBack -> {
                        navigator.navigateUp()
                    }
                    LogInEmailViewModel.Event.GoToLogInScreen -> {
                        goToLogInScreenAnimation(
                            coroutinesScope,
                            passwordVisibilityAnimation,
                            forgotPasswordAnimationOffsetY
                        )
                    }
                    is LogInEmailViewModel.Event.ForgotPasswordFailed -> {
                        context.shoErrorToast(event.errorInfo)
                    }
                    is LogInEmailViewModel.Event.ForgotPasswordSuccess -> {
                        context.showSuccessToast(event.infoMessage)
                    }
                    is LogInEmailViewModel.Event.LogInFailed -> {
                        context.shoErrorToast(event.errorInfo)
                    }
                }
            }
        }
    }

    FullscreenImage(gradientColors = listOf(dark_gray, Color.Transparent, Color.Transparent))

    Column(Modifier.padding(start = 6.dp, end = 6.dp, top = 16.dp)) {
        val lastFieldKeyboardAction = KeyboardActions(onDone = {
            keyboardController?.hide()
            focusManager.clearFocus()
        })

        val emailFieldKeyboardAction = if (screenState.loginState == LogInEmailViewModel.LoginState.FORGOT_PASSWORD) {
            lastFieldKeyboardAction
        } else
            KeyboardActions { focusManager.moveFocus(FocusDirection.Down) }

        InputField(
            label = stringResource(id = R.string.onboarding__email_registration),
            value = screenState.email,
            onValueChange = { newEmail ->
                logInEmailViewModel.onEvent(LogInEvent.UpdateEmail(newEmail))
            },
            errorText = screenState.emailError?.getString(),
            focusManager = focusManager,
            keyboardActions = emailFieldKeyboardAction
        )
        PasswordInputField(
            label = stringResource(id = R.string.onboarding__password_registration),
            value = screenState.password,
            onValueChange = { newPassword ->
                logInEmailViewModel.onEvent(LogInEvent.UpdatePassword(newPassword))
            },
            errorText = screenState.passwordError?.getString(),
            focusManager = focusManager,
            passwordVisibility = screenState.isPasswordVisible,
            changePasswordVisibility = {
                logInEmailViewModel.onEvent(LogInEvent.UpdatePasswordVisibility(it))
            },
            modifier = Modifier.alpha(passwordVisibilityAnimation.value),
            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Done),
            keyboardActions = lastFieldKeyboardAction
        )
        Column(
            Modifier
                .fillMaxWidth()
                .offset(y = forgotPasswordAnimationOffsetY.value.dp)
        ) {
            ActionButtonBlock(screenState.loginState.actionButtonText.getString()) {
                logInEmailViewModel.onEvent(LogInEvent.Action)
            }
            if (screenState.loginState == LogInEmailViewModel.LoginState.LOGIN) {
                ForgotPasswordBlock {
                    logInEmailViewModel.onEvent(LogInEvent.ForgotPassword)
                }
            }
        }
    }

    BackHandler {
        logInEmailViewModel.onEvent(LogInEvent.BackPressed)
    }
}

@Composable
private fun ActionButtonBlock(actionTest: String, onClick: () -> Unit) {
    Button(
        onClick = { onClick() },
        shape = RoundedCornerShape(24.dp),
        colors = ButtonDefaults.buttonColors(
            backgroundColor = dark_blue.copy(alpha = 0.8f),
            contentColor = white
        ),
        modifier = Modifier
            .fillMaxWidth()
            .padding(top = 32.dp)
    ) {
        Text(
            text = actionTest,
            modifier = Modifier.padding(top = 6.dp, bottom = 6.dp),
            fontSize = 16.sp,
            fontWeight = FontWeight.ExtraBold
        )
    }
}

@Composable
private fun ForgotPasswordBlock(onClick: () -> Unit) {
    Box(
        modifier = Modifier
            .padding(start = 8.dp, top = 12.dp)
            .clickable {
                onClick()
            }
    ) {
        Text(
            text = stringResource(id = R.string.onboarding__forgot_password),
            modifier = Modifier.padding(top = 6.dp, end = 6.dp, start = 6.dp, bottom = 6.dp),
            fontSize = 16.sp,
            fontWeight = FontWeight.ExtraBold,
            color = white
        )
    }
}

private fun goToForgotPasswordScreenAnimation(
    coroutinesScope: CoroutineScope,
    passwordVisibilityAnimation: Animatable<Float, AnimationVector1D>,
    forgotPasswordAnimationOffsetY: Animatable<Float, AnimationVector1D>
) {
    coroutinesScope.launch {
        passwordVisibilityAnimation.animateTo(INVISIBLE, animationSpec = tween(LONG_ANIMATION_TIME))
    }
    coroutinesScope.launch {
        forgotPasswordAnimationOffsetY.animateTo(
            FORGOT_PASSWORD_TARGET_ANIMATED_OFFSET,
            animationSpec = tween(LONG_ANIMATION_TIME)
        )
    }
}

private fun goToLogInScreenAnimation(
    coroutinesScope: CoroutineScope,
    passwordVisibilityAnimation: Animatable<Float, AnimationVector1D>,
    forgotPasswordAnimationOffsetY: Animatable<Float, AnimationVector1D>
) {
    coroutinesScope.launch {
        passwordVisibilityAnimation.animateTo(VISIBLE, animationSpec = tween(LONG_ANIMATION_TIME))
    }
    coroutinesScope.launch {
        forgotPasswordAnimationOffsetY.animateTo(
            FORGOT_PASSWORD_DEFAULT_OFFSET,
            animationSpec = tween(LONG_ANIMATION_TIME)
        )
    }
\$\endgroup\$
1
  • \$\begingroup\$ This is unfortunately just above my skills, but I think it's a good bit of code for code-review. As some-one who's 'getting into mvvm' this looks like it'll be useful in a week or so. Hopefully someone will review it. You posted this 6 months ago, could you review it yourself ? \$\endgroup\$
    – Lozminda
    Commented Jan 23, 2023 at 8:25

0

Browse other questions tagged or ask your own question.