From 0ca6ac9c9a40ae44009e1767e699e9b0b20f2998 Mon Sep 17 00:00:00 2001 From: Alex Gabor Date: Tue, 1 Mar 2022 12:49:10 +0200 Subject: [PATCH] Improve auth screen --- app/build.gradle | 1 + .../ui/compose/screen/auth/AuthScreen.kt | 112 ++++++++++++------ .../ui/compose/screen/auth/AuthScreenState.kt | 14 ++- 3 files changed, 83 insertions(+), 44 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index a33ff6c..3fa3f9c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -72,6 +72,7 @@ dependencies { implementation "androidx.appcompat:appcompat:$androidx_appcompat_version" implementation "com.google.android.material:material:$androidx_material_version" implementation "androidx.constraintlayout:constraintlayout:$androidx_constraintlayout_version" + implementation "androidx.constraintlayout:constraintlayout-compose:1.0.0" implementation "androidx.lifecycle:lifecycle-livedata-core-ktx:$androidx_livedata_version" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$androidx_livedata_version" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$androidx_livedata_version" diff --git a/app/src/main/java/org/fnives/test/showcase/ui/compose/screen/auth/AuthScreen.kt b/app/src/main/java/org/fnives/test/showcase/ui/compose/screen/auth/AuthScreen.kt index c53a2ea..2bfe613 100644 --- a/app/src/main/java/org/fnives/test/showcase/ui/compose/screen/auth/AuthScreen.kt +++ b/app/src/main/java/org/fnives/test/showcase/ui/compose/screen/auth/AuthScreen.kt @@ -1,72 +1,106 @@ package org.fnives.test.showcase.ui.compose.screen.auth import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp -import com.google.accompanist.insets.systemBarsPadding +import androidx.constraintlayout.compose.ConstraintLayout +import com.google.accompanist.insets.statusBarsPadding import org.fnives.test.showcase.R @Composable fun AuthScreen( authScreenState: AuthScreenState = rememberAuthScreenState() ) { - Column( - Modifier - .fillMaxSize() - .systemBarsPadding() - ) { - Title() - Column( + ConstraintLayout(Modifier.fillMaxSize()) { + val (title, credentials, snackbar, loading, login) = createRefs() + Title( Modifier - .fillMaxWidth() - .padding(16.dp) - .weight(1f), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - TextField( - value = authScreenState.username, - onValueChange = { authScreenState.onUsernameChanged(it) }, - modifier = Modifier.fillMaxWidth() - ) - TextField( - value = authScreenState.password, - onValueChange = { authScreenState.onPasswordChanged(it) }, - visualTransformation = PasswordVisualTransformation(), - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp) - ) + .statusBarsPadding() + .constrainAs(title) { top.linkTo(parent.top) }) + CredentialsFields(authScreenState, Modifier.constrainAs(credentials) { + top.linkTo(title.bottom) + bottom.linkTo(login.top) + }) + Snackbar(authScreenState, Modifier.constrainAs(snackbar) { + bottom.linkTo(login.top) + }) + if (authScreenState.loading) { + CircularProgressIndicator(Modifier.constrainAs(loading) { + bottom.linkTo(login.top) + centerHorizontallyTo(parent) + }) } - - Snackbar(authScreenState) LoginButton( modifier = Modifier - .align(Alignment.CenterHorizontally) + .constrainAs(login) { bottom.linkTo(parent.bottom) } .padding(16.dp), onClick = { authScreenState.onLogin() } ) } } +@OptIn(ExperimentalComposeUiApi::class) @Composable -private fun Snackbar(authScreenState: AuthScreenState) { +private fun CredentialsFields(authScreenState: AuthScreenState, modifier: Modifier = Modifier) { + val keyboardController = LocalSoftwareKeyboardController.current + Column( + modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + OutlinedTextField( + value = authScreenState.username, + label = { Text(text = stringResource(id = R.string.username)) }, + placeholder = { Text(text = stringResource(id = R.string.username)) }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email, imeAction = ImeAction.Next), + onValueChange = { authScreenState.onUsernameChanged(it) }, + modifier = Modifier.fillMaxWidth() + ) + OutlinedTextField( + value = authScreenState.password, + label = { Text(text = stringResource(id = R.string.password)) }, + placeholder = { Text(text = stringResource(id = R.string.password)) }, + onValueChange = { authScreenState.onPasswordChanged(it) }, + keyboardOptions = KeyboardOptions(autoCorrect = false, imeAction = ImeAction.Done, keyboardType = KeyboardType.Password), + keyboardActions = KeyboardActions(onDone = { + keyboardController?.hide() + authScreenState.onLogin() + }), + visualTransformation = PasswordVisualTransformation(), + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp) + ) + } +} + +@Composable +private fun Snackbar(authScreenState: AuthScreenState, modifier: Modifier = Modifier) { val snackbarState = remember { SnackbarHostState() } - val errorType = authScreenState.error?.consume() - LaunchedEffect(errorType) { - if (errorType != null) { - snackbarState.showSnackbar(errorType.name) + val error = authScreenState.error + LaunchedEffect(error) { + if (error != null) { + snackbarState.showSnackbar(error.name) + authScreenState.dismissError() } } - SnackbarHost(hostState = snackbarState) { - val stringId = errorType?.stringResId() + SnackbarHost(hostState = snackbarState, modifier) { + val stringId = error?.stringResId() if (stringId != null) { Snackbar(modifier = Modifier.padding(horizontal = 16.dp)) { Text(text = stringResource(stringId)) @@ -85,10 +119,10 @@ private fun LoginButton(modifier: Modifier = Modifier, onClick: () -> Unit) { } @Composable -private fun Title() { +private fun Title(modifier: Modifier = Modifier) { Text( stringResource(id = R.string.login_title), - modifier = Modifier.padding(16.dp), + modifier = modifier.padding(16.dp), style = MaterialTheme.typography.h4 ) } diff --git a/app/src/main/java/org/fnives/test/showcase/ui/compose/screen/auth/AuthScreenState.kt b/app/src/main/java/org/fnives/test/showcase/ui/compose/screen/auth/AuthScreenState.kt index c461da3..360def2 100644 --- a/app/src/main/java/org/fnives/test/showcase/ui/compose/screen/auth/AuthScreenState.kt +++ b/app/src/main/java/org/fnives/test/showcase/ui/compose/screen/auth/AuthScreenState.kt @@ -29,7 +29,7 @@ class AuthScreenState( private set var loading by mutableStateOf(false) private set - var error by mutableStateOf?>(null) + var error by mutableStateOf(null) private set var navigateToHome by mutableStateOf?>(null) private set @@ -53,7 +53,7 @@ class AuthScreenState( password = password ) when (val response = loginUseCase.invoke(credentials)) { - is Answer.Error -> error = Event(ErrorType.GENERAL_NETWORK_ERROR) + is Answer.Error -> error = ErrorType.GENERAL_NETWORK_ERROR is Answer.Success -> processLoginStatus(response.data) } loading = false @@ -63,13 +63,17 @@ class AuthScreenState( private fun processLoginStatus(loginStatus: LoginStatus) { when (loginStatus) { LoginStatus.SUCCESS -> navigateToHome = Event(Unit) - LoginStatus.INVALID_CREDENTIALS -> error = Event(ErrorType.INVALID_CREDENTIALS) - LoginStatus.INVALID_USERNAME -> error = Event(ErrorType.UNSUPPORTED_USERNAME) - LoginStatus.INVALID_PASSWORD -> error = Event(ErrorType.UNSUPPORTED_PASSWORD) + LoginStatus.INVALID_CREDENTIALS -> error = ErrorType.INVALID_CREDENTIALS + LoginStatus.INVALID_USERNAME -> error = ErrorType.UNSUPPORTED_USERNAME + LoginStatus.INVALID_PASSWORD -> error = ErrorType.UNSUPPORTED_PASSWORD } println("asdasdasd: ${error.hashCode()}") } + fun dismissError() { + error = null + } + enum class ErrorType { INVALID_CREDENTIALS, GENERAL_NETWORK_ERROR,