ViewModel refactor so Preview work.

This commit is contained in:
Thomas Andres Gomez 2021-05-08 15:33:24 +02:00
parent 45d2fe1336
commit 903d1973c2
16 changed files with 404 additions and 302 deletions

View file

@ -1,10 +1,9 @@
package com.pixelized.biblib.ui.composable.items
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
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
@ -12,6 +11,8 @@ 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
@ -53,25 +54,42 @@ fun WaitingComposable(
targetState = visible
) {
if (it) {
Card(elevation = 8.dp) {
Column(
Box(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.clickable { }
) {
Box(
modifier = Modifier
.width(200.dp)
.padding(16.dp)
.fillMaxWidth()
.fillMaxHeight()
.alpha(.25f)
.background(Color.Black)
)
Card(
elevation = 8.dp,
modifier = Modifier.align(Alignment.Center)
) {
CircularProgressIndicator(
Column(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.width(200.dp)
.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
) {
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
)
}
}
}
}

View file

@ -12,8 +12,9 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.pixelized.biblib.ui.composable.items.BookThumbnailComposable
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.ui.viewmodel.NavigationViewModel
import com.pixelized.biblib.ui.viewmodel.NavigationViewModel.Page.Detail
import com.pixelized.biblib.ui.viewmodel.navigation.INavigation
import com.pixelized.biblib.ui.viewmodel.navigation.INavigation.Page.Detail
import com.pixelized.biblib.ui.viewmodel.navigation.NavigationViewModel
import com.pixelized.biblib.utils.mock.BookMock
import com.pixelized.biblib.utils.mock.BookThumbnailMock
@ -21,12 +22,13 @@ import com.pixelized.biblib.utils.mock.BookThumbnailMock
@Composable
fun HomePageComposablePreview() {
BibLibTheme {
HomePageComposable(null)
val navigation = NavigationViewModel()
HomePageComposable(navigation)
}
}
@Composable
fun HomePageComposable(navigationViewModel: NavigationViewModel?) {
fun HomePageComposable(navigation: INavigation) {
val mock = BookThumbnailMock()
LazyColumn(
contentPadding = PaddingValues(16.dp),
@ -41,7 +43,7 @@ fun HomePageComposable(navigationViewModel: NavigationViewModel?) {
) { item ->
// TODO:
val bookMock = BookMock().let { it.books[item.id] ?: it.book }
navigationViewModel?.navigateTo(Detail(bookMock))
navigation.navigateTo(Detail(bookMock))
}
}
}

View file

@ -1,7 +1,6 @@
package com.pixelized.biblib.ui.composable.screen
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@ -29,142 +28,140 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
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.repository.googlesignin.IGoogleSingInRepository.AuthenticationState.Loading
import com.pixelized.biblib.ui.composable.items.WaitingComposable
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.ui.viewmodel.AuthenticationViewModel
import com.pixelized.biblib.ui.viewmodel.NavigationViewModel
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
@Preview
@Composable
fun LoginScreenComposablePreview() {
BibLibTheme {
val navigationViewModel = NavigationViewModel()
val authenticationViewModel = AuthenticationViewModel()
LoginScreenComposable(navigationViewModel, authenticationViewModel)
}
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun LoginScreenComposable(
navigationViewModel: NavigationViewModel,
authenticationViewModel: AuthenticationViewModel,
navigation: INavigation = viewModel<NavigationViewModel>(),
authentication: IAuthentication = viewModel<AuthenticationViewModel>(),
) {
Box(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
) {
val loginWithGoogleRequest = authenticationViewModel.prepareLoginWithGoogle()
val typography = MaterialTheme.typography
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.verticalScroll(rememberScrollState())
.padding(16.dp)
) {
Spacer(modifier = Modifier.weight(1f))
Text(
modifier = Modifier
.padding(vertical = 16.dp)
.align(alignment = Alignment.CenterHorizontally),
style = typography.h4,
text = stringResource(id = R.string.welcome_sign_in)
)
Spacer(modifier = Modifier.weight(1f))
val focusRequester = remember { FocusRequester() }
val localFocus = LocalFocusManager.current
LoginField(
viewModel = authenticationViewModel,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
keyboardActions = KeyboardActions { focusRequester.requestFocus() }
)
PasswordField(
viewModel = authenticationViewModel,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp)
.focusRequester(focusRequester),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions { localFocus.clearFocus() }
)
CredentialRemember(
viewModel = authenticationViewModel,
modifier = Modifier
.height(48.dp)
.padding(bottom = 16.dp)
)
Row(
modifier = Modifier
.padding(bottom = 16.dp)
.align(Alignment.End)
) {
Button(
modifier = Modifier.padding(end = 8.dp),
colors = outlinedButtonColors(),
onClick = {
authenticationViewModel.register()
}) {
Text(text = stringResource(id = R.string.action_register))
}
Button(onClick = {
authenticationViewModel.login()
}) {
Text(text = stringResource(id = R.string.action_login))
}
}
Spacer(modifier = Modifier.weight(2f))
Button(
modifier = Modifier.fillMaxWidth(),
colors = outlinedButtonColors(),
onClick = {
authenticationViewModel.loginWithGoogle(loginWithGoogleRequest)
}) {
Image(
modifier = Modifier.padding(end = 8.dp),
painter = painterResource(id = R.drawable.ic_google), contentDescription = ""
)
Text(text = stringResource(id = R.string.action_google_sign_in))
}
}
var waiting: Boolean by remember { mutableStateOf(false) }
waiting = when (loginWithGoogleRequest.result.value) {
is Loading -> true
else -> false
}
WaitingComposable(
modifier = Modifier.align(Alignment.Center),
visible = waiting,
message = stringResource(id = R.string.loading)
)
authentication.PrepareLoginWithGoogle()
LoginScreenContentComposable(navigation, authentication)
LoginScreenWaitingComposable(authentication)
}
}
@Composable
private fun LoginScreenContentComposable(
navigation: INavigation,
authentication: IAuthentication,
) {
val typography = MaterialTheme.typography
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.verticalScroll(rememberScrollState())
.padding(16.dp)
) {
Spacer(modifier = Modifier.weight(1f))
Text(
modifier = Modifier
.padding(vertical = 16.dp)
.align(alignment = Alignment.CenterHorizontally),
style = typography.h4,
text = stringResource(id = R.string.welcome_sign_in)
)
Spacer(modifier = Modifier.weight(1f))
val focusRequester = remember { FocusRequester() }
val localFocus = LocalFocusManager.current
LoginField(
authentication = authentication,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
keyboardActions = KeyboardActions { focusRequester.requestFocus() }
)
PasswordField(
authentication = authentication,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp)
.focusRequester(focusRequester),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions { localFocus.clearFocus() }
)
CredentialRemember(
authentication = authentication,
modifier = Modifier
.height(48.dp)
.padding(bottom = 16.dp)
)
Row(
modifier = Modifier
.padding(bottom = 16.dp)
.align(Alignment.End)
) {
Button(
modifier = Modifier.padding(end = 8.dp),
colors = outlinedButtonColors(),
onClick = {
authentication.register()
}) {
Text(text = stringResource(id = R.string.action_register))
}
Button(onClick = {
authentication.login()
}) {
Text(text = stringResource(id = R.string.action_login))
}
}
Spacer(modifier = Modifier.weight(2f))
Button(
modifier = Modifier.fillMaxWidth(),
colors = outlinedButtonColors(),
onClick = {
authentication.loginWithGoogle()
}) {
Image(
modifier = Modifier.padding(end = 8.dp),
painter = painterResource(id = R.drawable.ic_google), contentDescription = ""
)
Text(text = stringResource(id = R.string.action_google_sign_in))
}
}
}
@Composable
private fun LoginScreenWaitingComposable(authentication: IAuthentication) {
val state = authentication.state.observeAsState()
WaitingComposable(
visible = state.value is IAuthentication.State.Loading,
message = stringResource(id = R.string.loading)
)
}
@Composable
private fun LoginField(
viewModel: AuthenticationViewModel,
authentication: IAuthentication,
modifier: Modifier = Modifier,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions(),
) {
val login: State<String?> = viewModel.login.observeAsState()
val login: State<String?> = authentication.login.observeAsState()
TextField(
modifier = modifier,
value = login.value ?: "",
onValueChange = { viewModel.updateLoginField(it) },
onValueChange = { authentication.updateLoginField(it) },
label = { Text(text = stringResource(id = R.string.authentication_login)) },
colors = outlinedTextFieldColors(),
maxLines = 1,
@ -176,17 +173,17 @@ private fun LoginField(
@Composable
private fun PasswordField(
viewModel: AuthenticationViewModel,
authentication: IAuthentication,
modifier: Modifier = Modifier,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions(),
) {
val password = viewModel.password.observeAsState()
val password = authentication.password.observeAsState()
var passwordVisibility by remember { mutableStateOf(false) }
TextField(
modifier = modifier,
value = password.value ?: "",
onValueChange = { viewModel.updatePasswordField(it) },
onValueChange = { authentication.updatePasswordField(it) },
label = { Text(text = stringResource(id = R.string.authentication_password)) },
colors = outlinedTextFieldColors(),
maxLines = 1,
@ -206,10 +203,15 @@ private fun PasswordField(
}
@Composable
private fun CredentialRemember(viewModel: AuthenticationViewModel, modifier: Modifier = Modifier) {
val credential = viewModel.rememberCredential.observeAsState()
private fun CredentialRemember(
authentication: IAuthentication,
modifier: Modifier = Modifier
) {
val credential = authentication.rememberCredential.observeAsState()
Row(modifier = modifier.clickable {
viewModel.updateRememberCredential(rememberCredential = credential.value?.not() ?: false)
authentication.updateRememberCredential(
rememberCredential = credential.value?.not() ?: false
)
}) {
Checkbox(
modifier = Modifier.align(Alignment.CenterVertically),
@ -223,4 +225,24 @@ private fun CredentialRemember(viewModel: AuthenticationViewModel, modifier: Mod
text = stringResource(id = R.string.authentication_credential_remember)
)
}
}
@Preview
@Composable
fun LoginScreenComposablePreview() {
BibLibTheme {
val navigationViewModel = INavigation.Mock()
val authenticationViewModel = IAuthentication.Mock()
LoginScreenComposable(navigationViewModel, authenticationViewModel)
}
}
@Preview
@Composable
fun LoginScreenComposableWaitingPreview() {
BibLibTheme {
val navigationViewModel = INavigation.Mock()
val authenticationViewModel = IAuthentication.Mock(true)
LoginScreenComposable(navigationViewModel, authenticationViewModel)
}
}

View file

@ -11,28 +11,28 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.viewmodel.compose.viewModel
import com.pixelized.biblib.R
import com.pixelized.biblib.ui.composable.pages.DetailPageComposable
import com.pixelized.biblib.ui.composable.pages.HomePageComposable
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.ui.viewmodel.NavigationViewModel
import com.pixelized.biblib.ui.viewmodel.NavigationViewModel.Page
import com.pixelized.biblib.ui.viewmodel.navigation.INavigation
import com.pixelized.biblib.ui.viewmodel.navigation.INavigation.Page
import com.pixelized.biblib.ui.viewmodel.navigation.NavigationViewModel
@Preview
@Composable
fun ToolbarComposableDarkPreview() {
val viewModel = NavigationViewModel()
BibLibTheme(darkTheme = false) {
ToolbarComposable(navigationViewModel = viewModel)
ToolbarComposable(navigation = INavigation.Mock())
}
}
@Preview
@Composable
fun ToolbarComposableLightPreview() {
val viewModel = NavigationViewModel()
BibLibTheme(darkTheme = true) {
ToolbarComposable(navigationViewModel = viewModel)
ToolbarComposable(navigation = INavigation.Mock())
}
}
@ -40,27 +40,26 @@ fun ToolbarComposableLightPreview() {
@Composable
fun MainScreenComposablePreview() {
BibLibTheme {
val viewModel = NavigationViewModel()
MainScreenComposable(viewModel)
MainScreenComposable(INavigation.Mock(page = Page.HomePage))
}
}
@Composable
fun MainScreenComposable(
navigationViewModel: NavigationViewModel
navigation: INavigation = viewModel<NavigationViewModel>()
) {
val page by navigationViewModel.page.observeAsState()
val page by navigation.page.observeAsState()
LaunchedEffect(key1 = "MainScreen", block = {
navigationViewModel.navigateTo(Page.HomePage)
navigation.navigateTo(Page.HomePage)
})
Scaffold(
topBar = { ToolbarComposable(navigationViewModel) },
topBar = { ToolbarComposable(navigation) },
) {
Crossfade(targetState = page) {
when (it) {
is Page.HomePage -> HomePageComposable(navigationViewModel)
is Page.HomePage -> HomePageComposable(navigation)
is Page.Detail -> DetailPageComposable(it.book)
}
}
@ -68,16 +67,16 @@ fun MainScreenComposable(
}
@Composable
fun ToolbarComposable(navigationViewModel: NavigationViewModel) {
fun ToolbarComposable(navigation: INavigation) {
TopAppBar(
title = { Text(stringResource(id = R.string.app_name)) },
navigationIcon = { NavigationIcon(navigationViewModel) }
navigationIcon = { NavigationIcon(navigation) }
)
}
@Composable
fun NavigationIcon(navigationViewModel: NavigationViewModel) {
val page: Page? by navigationViewModel.page.observeAsState()
fun NavigationIcon(navigation: INavigation) {
val page: Page? by navigation.page.observeAsState()
Crossfade(targetState = page) {
when (it) {
@ -88,7 +87,7 @@ fun NavigationIcon(navigationViewModel: NavigationViewModel) {
)
}
else -> IconButton(onClick = {
navigationViewModel.navigateBack()
navigation.navigateBack()
}) {
Icon(
imageVector = Icons.Sharp.ArrowBack,

View file

@ -11,8 +11,10 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.viewmodel.compose.viewModel
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.ui.viewmodel.NavigationViewModel
import com.pixelized.biblib.ui.viewmodel.navigation.INavigation
import com.pixelized.biblib.ui.viewmodel.navigation.NavigationViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@ -21,14 +23,13 @@ import kotlinx.coroutines.launch
@Composable
fun SplashScreenComposablePreview() {
BibLibTheme {
val viewModel = NavigationViewModel()
SplashScreenComposable(viewModel)
SplashScreenComposable(INavigation.Mock())
}
}
@Composable
fun SplashScreenComposable(
navigationViewModel: NavigationViewModel
navigation: INavigation = viewModel<NavigationViewModel>()
) {
val typography = MaterialTheme.typography
@ -48,7 +49,7 @@ fun SplashScreenComposable(
LaunchedEffect(key1 = "loading", block = {
coroutineScope.launch {
delay(1000)
navigationViewModel.navigateTo(NavigationViewModel.Screen.LoginScreen)
navigation.navigateTo(INavigation.Screen.LoginScreen)
}
})
}