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)
)
}