Improve auth screen

This commit is contained in:
Alex Gabor 2022-03-01 12:49:10 +02:00
parent b6e4d282b7
commit 0ca6ac9c9a
3 changed files with 83 additions and 44 deletions

View file

@ -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"

View file

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

View file

@ -29,7 +29,7 @@ class AuthScreenState(
private set
var loading by mutableStateOf(false)
private set
var error by mutableStateOf<Event<ErrorType>?>(null)
var error by mutableStateOf<ErrorType?>(null)
private set
var navigateToHome by mutableStateOf<Event<Unit>?>(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,