Add loading and error dialogs for authentication.

This commit is contained in:
Thomas Andres Gomez 2021-05-08 21:07:17 +02:00
parent b07bfd45d3
commit 59d84963a9
11 changed files with 296 additions and 127 deletions

View file

@ -1,100 +0,0 @@
package com.pixelized.biblib.ui.composable.items
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.Card
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.pixelized.biblib.R
import com.pixelized.biblib.ui.theme.BibLibTheme
@Preview
@Composable
fun WaitingComposableLightPreview() {
BibLibTheme(darkTheme = false) {
WaitingComposable(
visible = true,
message = stringResource(id = R.string.loading)
)
}
}
@Preview
@Composable
fun WaitingComposableDarkPreview() {
BibLibTheme(darkTheme = true) {
WaitingComposable(
visible = true,
message = stringResource(id = R.string.loading)
)
}
}
@Composable
fun WaitingComposable(
visible: Boolean,
modifier: Modifier = Modifier,
message: String? = null
) {
Crossfade(
modifier = modifier,
targetState = visible
) {
if (it) {
Box(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.clickable { }
) {
Box(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.alpha(.25f)
.background(Color.Black)
)
Card(
elevation = 8.dp,
modifier = Modifier.align(Alignment.Center)
) {
Column(
modifier = Modifier
.width(200.dp)
.padding(16.dp)
) {
CircularProgressIndicator(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(16.dp)
)
if (message?.isNotEmpty() == true) {
val typography = MaterialTheme.typography
Text(
modifier = Modifier.align(Alignment.CenterHorizontally),
style = typography.body1,
textAlign = TextAlign.Center,
text = message
)
}
}
}
}
} else {
Box {}
}
}
}

View file

@ -0,0 +1,82 @@
package com.pixelized.biblib.ui.composable.items.dialog
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.sharp.ErrorOutline
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.pixelized.biblib.R
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.utils.exception.NoBearerException
@Composable
fun ErrorCard(
modifier: Modifier = Modifier,
message: String = stringResource(id = R.string.error_generic),
exception: Exception? = null,
) {
Card(elevation = 8.dp, modifier = modifier) {
Column(
modifier = Modifier
.width(200.dp)
.padding(16.dp)
) {
Icon(
modifier = Modifier
.width(72.dp)
.height(72.dp)
.align(Alignment.CenterHorizontally)
.padding(16.dp),
tint = MaterialTheme.colors.error,
imageVector = Icons.Sharp.ErrorOutline,
contentDescription = "error"
)
val typography = MaterialTheme.typography
Text(
modifier = Modifier.align(Alignment.CenterHorizontally),
style = typography.body1,
textAlign = TextAlign.Center,
text = message
)
if (exception != null) {
Text(
modifier = Modifier
.padding(top = 8.dp)
.align(Alignment.CenterHorizontally),
style = typography.caption,
textAlign = TextAlign.Center,
text = exception::class.java.simpleName
)
}
}
}
}
@Preview
@Composable
private fun ErrorCardLightPreview() {
BibLibTheme(darkTheme = false) {
ErrorCard(exception = NoBearerException())
}
}
@Preview
@Composable
private fun ErrorCardDarkPreview() {
BibLibTheme(darkTheme = true) {
ErrorCard(exception = NoBearerException())
}
}

View file

@ -0,0 +1,68 @@
package com.pixelized.biblib.ui.composable.items.dialog
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.Card
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.pixelized.biblib.R
import com.pixelized.biblib.ui.theme.BibLibTheme
@Composable
fun LoadingCard(
modifier: Modifier = Modifier,
message: String? = null
) {
Card(elevation = 8.dp, modifier = modifier) {
Column(
modifier = Modifier
.width(200.dp)
.padding(16.dp)
) {
CircularProgressIndicator(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(16.dp)
)
if (message?.isNotEmpty() == true) {
val typography = MaterialTheme.typography
Text(
modifier = Modifier.align(Alignment.CenterHorizontally),
style = typography.body1,
textAlign = TextAlign.Center,
text = message
)
}
}
}
}
@Preview
@Composable
private fun LoadingCardLightPreview() {
BibLibTheme(darkTheme = false) {
LoadingCard(
message = stringResource(id = R.string.loading)
)
}
}
@Preview
@Composable
private fun LoadingCardDarkPreview() {
BibLibTheme(darkTheme = true) {
LoadingCard(
message = stringResource(id = R.string.loading)
)
}
}

View file

@ -0,0 +1,43 @@
package com.pixelized.biblib.ui.composable.items.dialog
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
@Composable
fun CrossFadeOverlay(
visible: Boolean,
content: @Composable BoxScope.() -> Unit
) {
Crossfade(targetState = visible) {
if (it) {
Box(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.clickable { }
) {
// Transparent background.
Box(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.alpha(.25f)
.background(Color.Black)
)
// Overlay content.
content()
}
} else {
Box {}
}
}
}

View file

@ -30,12 +30,15 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.pixelized.biblib.R
import com.pixelized.biblib.ui.composable.items.WaitingComposable
import com.pixelized.biblib.ui.composable.items.dialog.CrossFadeOverlay
import com.pixelized.biblib.ui.composable.items.dialog.ErrorCard
import com.pixelized.biblib.ui.composable.items.dialog.LoadingCard
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.ui.viewmodel.authentication.AuthenticationViewModel
import com.pixelized.biblib.ui.viewmodel.authentication.IAuthentication
import com.pixelized.biblib.ui.viewmodel.navigation.INavigation
import com.pixelized.biblib.ui.viewmodel.navigation.NavigationViewModel
import com.pixelized.biblib.utils.exception.NoBearerException
@Composable
@ -49,14 +52,15 @@ fun LoginScreenComposable(
.fillMaxHeight()
) {
authentication.PrepareLoginWithGoogle()
LoginScreenContentComposable(navigation, authentication)
LoginScreenWaitingComposable(authentication)
LoginScreenNavigationComposable(navigation, authentication)
LoginScreenContentComposable(authentication)
LoginScreenDialogComposable(authentication)
}
}
@Composable
private fun LoginScreenContentComposable(
navigation: INavigation,
authentication: IAuthentication,
) {
val typography = MaterialTheme.typography
@ -142,12 +146,40 @@ private fun LoginScreenContentComposable(
}
@Composable
private fun LoginScreenWaitingComposable(authentication: IAuthentication) {
private fun LoginScreenDialogComposable(
authentication: IAuthentication
) {
val state = authentication.state.observeAsState()
WaitingComposable(
visible = state.value is IAuthentication.State.Loading,
message = stringResource(id = R.string.loading)
)
CrossFadeOverlay(
visible = (state.value is IAuthentication.State.Initial).not()
) {
when (val currentState = state.value) {
is IAuthentication.State.Error -> ErrorCard(
modifier = Modifier
.align(Alignment.Center)
.clickable { authentication.clearState() },
message = stringResource(id = R.string.error_generic),
exception = currentState.exception
)
is IAuthentication.State.Connect,
is IAuthentication.State.Loading -> LoadingCard(
modifier = Modifier.align(Alignment.Center),
message = stringResource(id = R.string.loading)
)
else -> Box {}
}
}
}
@Composable
private fun LoginScreenNavigationComposable(
navigation: INavigation,
authentication: IAuthentication
) {
val state = authentication.state.observeAsState()
if (state.value == IAuthentication.State.Connect) {
navigation.navigateTo(INavigation.Screen.MainScreen)
}
}
@Composable
@ -237,12 +269,24 @@ fun LoginScreenComposablePreview() {
}
}
@Preview
@Preview(name = "Loading")
@Composable
fun LoginScreenComposableWaitingPreview() {
BibLibTheme {
val navigationViewModel = INavigation.Mock()
val authenticationViewModel = IAuthentication.Mock(true)
val state = IAuthentication.State.Loading
val authenticationViewModel = IAuthentication.Mock(state)
LoginScreenComposable(navigationViewModel, authenticationViewModel)
}
}
@Preview(name = "Error")
@Composable
fun LoginScreenComposableErrorPreview() {
BibLibTheme {
val navigationViewModel = INavigation.Mock()
val state = IAuthentication.State.Error(NoBearerException())
val authenticationViewModel = IAuthentication.Mock(state)
LoginScreenComposable(navigationViewModel, authenticationViewModel)
}
}