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

11
.idea/misc.xml generated
View file

@ -96,6 +96,17 @@
<entry key="../../../../../layout/compose-model-1620467930629.xml" value="0.16300675675675674" /> <entry key="../../../../../layout/compose-model-1620467930629.xml" value="0.16300675675675674" />
<entry key="../../../../../layout/compose-model-1620468466480.xml" value="0.5818181818181818" /> <entry key="../../../../../layout/compose-model-1620468466480.xml" value="0.5818181818181818" />
<entry key="../../../../../layout/compose-model-1620469838058.xml" value="0.5818181818181818" /> <entry key="../../../../../layout/compose-model-1620469838058.xml" value="0.5818181818181818" />
<entry key="../../../../../layout/compose-model-1620475818485.xml" value="0.28125" />
<entry key="../../../../../layout/compose-model-1620476290583.xml" value="0.13349514563106796" />
<entry key="../../../../../layout/compose-model-1620476544881.xml" value="0.28125" />
<entry key="../../../../../layout/compose-model-1620476682460.xml" value="0.23514851485148514" />
<entry key="../../../../../layout/compose-model-1620477081886.xml" value="0.28125" />
<entry key="../../../../../layout/compose-model-1620479574036.xml" value="0.26725521669341895" />
<entry key="../../../../../layout/compose-model-1620479753480.xml" value="0.5818181818181818" />
<entry key="../../../../../layout/compose-model-1620479912289.xml" value="0.13900501672240803" />
<entry key="../../../../../layout/compose-model-1620480106062.xml" value="0.13900501672240803" />
<entry key="../../../../../layout/compose-model-1620480615996.xml" value="0.25" />
<entry key="../../../../../layout/compose-model-1620480616087.xml" value="0.13900501672240803" />
<entry key="app/src/main/res/drawable-v24/ic_launcher_foreground.xml" value="0.2898148148148148" /> <entry key="app/src/main/res/drawable-v24/ic_launcher_foreground.xml" value="0.2898148148148148" />
<entry key="app/src/main/res/drawable/ic_baseline_local_library_24.xml" value="0.25462962962962965" /> <entry key="app/src/main/res/drawable/ic_baseline_local_library_24.xml" value="0.25462962962962965" />
<entry key="app/src/main/res/drawable/ic_google.xml" value="0.2962962962962963" /> <entry key="app/src/main/res/drawable/ic_google.xml" value="0.2962962962962963" />

View file

@ -4,7 +4,7 @@ import android.app.Application
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.core.content.edit import androidx.core.content.edit
import com.pixelized.biblib.ui.viewmodel.AuthenticationViewModel import com.pixelized.biblib.ui.viewmodel.authentication.AuthenticationViewModel
class CredentialRepository(application: Application) : ICredentialRepository { class CredentialRepository(application: Application) : ICredentialRepository {
private val preferences = private val preferences =

View file

@ -1,56 +1,21 @@
package com.pixelized.biblib.repository.googlesignin package com.pixelized.biblib.repository.googlesignin
import android.app.Application import android.app.Application
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
import com.google.android.gms.auth.api.signin.GoogleSignInClient import com.google.android.gms.auth.api.signin.GoogleSignInClient
import com.google.android.gms.auth.api.signin.GoogleSignInOptions import com.google.android.gms.auth.api.signin.GoogleSignInOptions
import com.google.android.gms.common.api.ApiException
import com.pixelized.biblib.R import com.pixelized.biblib.R
import com.pixelized.biblib.utils.exception.MissingTokenException
class GoogleSingInRepository(application: Application) : IGoogleSingInRepository { class GoogleSingInRepository(application: Application) : IGoogleSingInRepository {
override val googleSignInOption: GoogleSignInOptions by lazy { override val option: GoogleSignInOptions by lazy {
GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(application.getString(R.string.biblib_server_id)) .requestIdToken(application.getString(R.string.biblib_server_id))
.requestEmail() .requestEmail()
.build() .build()
} }
override val googleSignIn: GoogleSignInClient by lazy { override val client: GoogleSignInClient by lazy {
GoogleSignIn.getClient(application, googleSignInOption) GoogleSignIn.getClient(application, option)
}
@Composable
override fun prepareLoginWithGoogle(): IGoogleSingInRepository.Request {
val result = remember {
mutableStateOf<IGoogleSingInRepository.AuthenticationState>(
IGoogleSingInRepository.AuthenticationState.Initial
)
}
val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
try {
val task = GoogleSignIn.getSignedInAccountFromIntent(it.data)
val account: GoogleSignInAccount? = task.getResult(ApiException::class.java)
val idToken = account?.idToken ?: throw MissingTokenException()
result.value = IGoogleSingInRepository.AuthenticationState.Connect(idToken)
} catch (exception: Exception) {
result.value = IGoogleSingInRepository.AuthenticationState.Error(exception)
}
}
return IGoogleSingInRepository.Request(result, launcher)
}
override fun loginWithGoogle(request: IGoogleSingInRepository.Request) {
request.result.value = IGoogleSingInRepository.AuthenticationState.Loading
request.launcher.launch(googleSignIn.signInIntent)
} }
} }

View file

@ -1,30 +1,9 @@
package com.pixelized.biblib.repository.googlesignin package com.pixelized.biblib.repository.googlesignin
import android.content.Intent
import androidx.activity.result.ActivityResultLauncher
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import com.google.android.gms.auth.api.signin.GoogleSignInClient import com.google.android.gms.auth.api.signin.GoogleSignInClient
import com.google.android.gms.auth.api.signin.GoogleSignInOptions import com.google.android.gms.auth.api.signin.GoogleSignInOptions
interface IGoogleSingInRepository { interface IGoogleSingInRepository {
val googleSignInOption: GoogleSignInOptions val option: GoogleSignInOptions
val googleSignIn: GoogleSignInClient val client: GoogleSignInClient
@Composable
fun prepareLoginWithGoogle(): Request
fun loginWithGoogle(request: Request)
data class Request(
val result: MutableState<AuthenticationState>,
val launcher: ActivityResultLauncher<Intent>,
)
sealed class AuthenticationState {
object Initial : AuthenticationState()
object Loading : AuthenticationState()
data class Connect(val token: String) : AuthenticationState()
data class Error(val exception: Exception) : AuthenticationState()
}
} }

View file

@ -9,15 +9,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
import com.pixelized.biblib.R
import com.pixelized.biblib.ui.composable.screen.LoginScreenComposable import com.pixelized.biblib.ui.composable.screen.LoginScreenComposable
import com.pixelized.biblib.ui.composable.screen.MainScreenComposable import com.pixelized.biblib.ui.composable.screen.MainScreenComposable
import com.pixelized.biblib.ui.composable.screen.SplashScreenComposable import com.pixelized.biblib.ui.composable.screen.SplashScreenComposable
import com.pixelized.biblib.ui.theme.BibLibTheme import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.ui.viewmodel.NavigationViewModel import com.pixelized.biblib.ui.viewmodel.navigation.NavigationViewModel
import com.pixelized.biblib.ui.viewmodel.NavigationViewModel.Screen import com.pixelized.biblib.ui.viewmodel.navigation.INavigation.Screen
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private val navigationViewModel: NavigationViewModel by viewModels() private val navigationViewModel: NavigationViewModel by viewModels()
@ -46,9 +43,9 @@ fun ContentComposable() {
Crossfade(targetState = main) { Crossfade(targetState = main) {
when (it) { when (it) {
is Screen.SplashScreen -> SplashScreenComposable(viewModel()) is Screen.SplashScreen -> SplashScreenComposable()
is Screen.LoginScreen -> LoginScreenComposable(viewModel(), viewModel()) is Screen.LoginScreen -> LoginScreenComposable()
is Screen.MainScreen -> MainScreenComposable(viewModel()) is Screen.MainScreen -> MainScreenComposable()
} }
} }
} }

View file

@ -1,10 +1,9 @@
package com.pixelized.biblib.ui.composable.items package com.pixelized.biblib.ui.composable.items
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.width
import androidx.compose.material.Card import androidx.compose.material.Card
import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
@ -12,6 +11,8 @@ import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@ -53,25 +54,42 @@ fun WaitingComposable(
targetState = visible targetState = visible
) { ) {
if (it) { if (it) {
Card(elevation = 8.dp) { Box(
Column( modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.clickable { }
) {
Box(
modifier = Modifier modifier = Modifier
.width(200.dp) .fillMaxWidth()
.padding(16.dp) .fillMaxHeight()
.alpha(.25f)
.background(Color.Black)
)
Card(
elevation = 8.dp,
modifier = Modifier.align(Alignment.Center)
) { ) {
CircularProgressIndicator( Column(
modifier = Modifier modifier = Modifier
.align(Alignment.CenterHorizontally) .width(200.dp)
.padding(16.dp) .padding(16.dp)
) ) {
if (message?.isNotEmpty() == true) { CircularProgressIndicator(
val typography = MaterialTheme.typography modifier = Modifier
Text( .align(Alignment.CenterHorizontally)
modifier = Modifier.align(Alignment.CenterHorizontally), .padding(16.dp)
style = typography.body1,
textAlign = TextAlign.Center,
text = message
) )
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 androidx.compose.ui.unit.dp
import com.pixelized.biblib.ui.composable.items.BookThumbnailComposable import com.pixelized.biblib.ui.composable.items.BookThumbnailComposable
import com.pixelized.biblib.ui.theme.BibLibTheme 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.NavigationViewModel.Page.Detail 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.BookMock
import com.pixelized.biblib.utils.mock.BookThumbnailMock import com.pixelized.biblib.utils.mock.BookThumbnailMock
@ -21,12 +22,13 @@ import com.pixelized.biblib.utils.mock.BookThumbnailMock
@Composable @Composable
fun HomePageComposablePreview() { fun HomePageComposablePreview() {
BibLibTheme { BibLibTheme {
HomePageComposable(null) val navigation = NavigationViewModel()
HomePageComposable(navigation)
} }
} }
@Composable @Composable
fun HomePageComposable(navigationViewModel: NavigationViewModel?) { fun HomePageComposable(navigation: INavigation) {
val mock = BookThumbnailMock() val mock = BookThumbnailMock()
LazyColumn( LazyColumn(
contentPadding = PaddingValues(16.dp), contentPadding = PaddingValues(16.dp),
@ -41,7 +43,7 @@ fun HomePageComposable(navigationViewModel: NavigationViewModel?) {
) { item -> ) { item ->
// TODO: // TODO:
val bookMock = BookMock().let { it.books[item.id] ?: it.book } 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 package com.pixelized.biblib.ui.composable.screen
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* 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.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.pixelized.biblib.R 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.composable.items.WaitingComposable
import com.pixelized.biblib.ui.theme.BibLibTheme import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.ui.viewmodel.AuthenticationViewModel import com.pixelized.biblib.ui.viewmodel.authentication.AuthenticationViewModel
import com.pixelized.biblib.ui.viewmodel.NavigationViewModel 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 @Composable
fun LoginScreenComposable( fun LoginScreenComposable(
navigationViewModel: NavigationViewModel, navigation: INavigation = viewModel<NavigationViewModel>(),
authenticationViewModel: AuthenticationViewModel, authentication: IAuthentication = viewModel<AuthenticationViewModel>(),
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.fillMaxHeight() .fillMaxHeight()
) { ) {
val loginWithGoogleRequest = authenticationViewModel.prepareLoginWithGoogle() authentication.PrepareLoginWithGoogle()
val typography = MaterialTheme.typography LoginScreenContentComposable(navigation, authentication)
LoginScreenWaitingComposable(authentication)
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)
)
} }
} }
@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 @Composable
private fun LoginField( private fun LoginField(
viewModel: AuthenticationViewModel, authentication: IAuthentication,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions(), keyboardActions: KeyboardActions = KeyboardActions(),
) { ) {
val login: State<String?> = viewModel.login.observeAsState() val login: State<String?> = authentication.login.observeAsState()
TextField( TextField(
modifier = modifier, modifier = modifier,
value = login.value ?: "", value = login.value ?: "",
onValueChange = { viewModel.updateLoginField(it) }, onValueChange = { authentication.updateLoginField(it) },
label = { Text(text = stringResource(id = R.string.authentication_login)) }, label = { Text(text = stringResource(id = R.string.authentication_login)) },
colors = outlinedTextFieldColors(), colors = outlinedTextFieldColors(),
maxLines = 1, maxLines = 1,
@ -176,17 +173,17 @@ private fun LoginField(
@Composable @Composable
private fun PasswordField( private fun PasswordField(
viewModel: AuthenticationViewModel, authentication: IAuthentication,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions(), keyboardActions: KeyboardActions = KeyboardActions(),
) { ) {
val password = viewModel.password.observeAsState() val password = authentication.password.observeAsState()
var passwordVisibility by remember { mutableStateOf(false) } var passwordVisibility by remember { mutableStateOf(false) }
TextField( TextField(
modifier = modifier, modifier = modifier,
value = password.value ?: "", value = password.value ?: "",
onValueChange = { viewModel.updatePasswordField(it) }, onValueChange = { authentication.updatePasswordField(it) },
label = { Text(text = stringResource(id = R.string.authentication_password)) }, label = { Text(text = stringResource(id = R.string.authentication_password)) },
colors = outlinedTextFieldColors(), colors = outlinedTextFieldColors(),
maxLines = 1, maxLines = 1,
@ -206,10 +203,15 @@ private fun PasswordField(
} }
@Composable @Composable
private fun CredentialRemember(viewModel: AuthenticationViewModel, modifier: Modifier = Modifier) { private fun CredentialRemember(
val credential = viewModel.rememberCredential.observeAsState() authentication: IAuthentication,
modifier: Modifier = Modifier
) {
val credential = authentication.rememberCredential.observeAsState()
Row(modifier = modifier.clickable { Row(modifier = modifier.clickable {
viewModel.updateRememberCredential(rememberCredential = credential.value?.not() ?: false) authentication.updateRememberCredential(
rememberCredential = credential.value?.not() ?: false
)
}) { }) {
Checkbox( Checkbox(
modifier = Modifier.align(Alignment.CenterVertically), modifier = Modifier.align(Alignment.CenterVertically),
@ -223,4 +225,24 @@ private fun CredentialRemember(viewModel: AuthenticationViewModel, modifier: Mod
text = stringResource(id = R.string.authentication_credential_remember) 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.runtime.livedata.observeAsState
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.viewmodel.compose.viewModel
import com.pixelized.biblib.R import com.pixelized.biblib.R
import com.pixelized.biblib.ui.composable.pages.DetailPageComposable import com.pixelized.biblib.ui.composable.pages.DetailPageComposable
import com.pixelized.biblib.ui.composable.pages.HomePageComposable import com.pixelized.biblib.ui.composable.pages.HomePageComposable
import com.pixelized.biblib.ui.theme.BibLibTheme 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.NavigationViewModel.Page import com.pixelized.biblib.ui.viewmodel.navigation.INavigation.Page
import com.pixelized.biblib.ui.viewmodel.navigation.NavigationViewModel
@Preview @Preview
@Composable @Composable
fun ToolbarComposableDarkPreview() { fun ToolbarComposableDarkPreview() {
val viewModel = NavigationViewModel()
BibLibTheme(darkTheme = false) { BibLibTheme(darkTheme = false) {
ToolbarComposable(navigationViewModel = viewModel) ToolbarComposable(navigation = INavigation.Mock())
} }
} }
@Preview @Preview
@Composable @Composable
fun ToolbarComposableLightPreview() { fun ToolbarComposableLightPreview() {
val viewModel = NavigationViewModel()
BibLibTheme(darkTheme = true) { BibLibTheme(darkTheme = true) {
ToolbarComposable(navigationViewModel = viewModel) ToolbarComposable(navigation = INavigation.Mock())
} }
} }
@ -40,27 +40,26 @@ fun ToolbarComposableLightPreview() {
@Composable @Composable
fun MainScreenComposablePreview() { fun MainScreenComposablePreview() {
BibLibTheme { BibLibTheme {
val viewModel = NavigationViewModel() MainScreenComposable(INavigation.Mock(page = Page.HomePage))
MainScreenComposable(viewModel)
} }
} }
@Composable @Composable
fun MainScreenComposable( fun MainScreenComposable(
navigationViewModel: NavigationViewModel navigation: INavigation = viewModel<NavigationViewModel>()
) { ) {
val page by navigationViewModel.page.observeAsState() val page by navigation.page.observeAsState()
LaunchedEffect(key1 = "MainScreen", block = { LaunchedEffect(key1 = "MainScreen", block = {
navigationViewModel.navigateTo(Page.HomePage) navigation.navigateTo(Page.HomePage)
}) })
Scaffold( Scaffold(
topBar = { ToolbarComposable(navigationViewModel) }, topBar = { ToolbarComposable(navigation) },
) { ) {
Crossfade(targetState = page) { Crossfade(targetState = page) {
when (it) { when (it) {
is Page.HomePage -> HomePageComposable(navigationViewModel) is Page.HomePage -> HomePageComposable(navigation)
is Page.Detail -> DetailPageComposable(it.book) is Page.Detail -> DetailPageComposable(it.book)
} }
} }
@ -68,16 +67,16 @@ fun MainScreenComposable(
} }
@Composable @Composable
fun ToolbarComposable(navigationViewModel: NavigationViewModel) { fun ToolbarComposable(navigation: INavigation) {
TopAppBar( TopAppBar(
title = { Text(stringResource(id = R.string.app_name)) }, title = { Text(stringResource(id = R.string.app_name)) },
navigationIcon = { NavigationIcon(navigationViewModel) } navigationIcon = { NavigationIcon(navigation) }
) )
} }
@Composable @Composable
fun NavigationIcon(navigationViewModel: NavigationViewModel) { fun NavigationIcon(navigation: INavigation) {
val page: Page? by navigationViewModel.page.observeAsState() val page: Page? by navigation.page.observeAsState()
Crossfade(targetState = page) { Crossfade(targetState = page) {
when (it) { when (it) {
@ -88,7 +87,7 @@ fun NavigationIcon(navigationViewModel: NavigationViewModel) {
) )
} }
else -> IconButton(onClick = { else -> IconButton(onClick = {
navigationViewModel.navigateBack() navigation.navigateBack()
}) { }) {
Icon( Icon(
imageVector = Icons.Sharp.ArrowBack, imageVector = Icons.Sharp.ArrowBack,

View file

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

View file

@ -1,53 +0,0 @@
package com.pixelized.biblib.ui.viewmodel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.biblib.data.ui.BookUio
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.*
class NavigationViewModel : ViewModel() {
private val stack = Stack<Page>()
private val _screen = MutableLiveData<Screen>(Screen.SplashScreen)
val screen: LiveData<Screen> get() = _screen
private val _page = MutableLiveData<Page>(Page.HomePage)
val page: LiveData<Page> get() = _page
fun navigateTo(screen: Screen): Boolean {
_screen.postValue(screen)
return true
}
fun navigateTo(page: Page): Boolean {
_page.postValue(page)
stack.push(page)
return true
}
fun navigateBack() : Boolean {
stack.pop()
return if (stack.empty()) {
false
} else {
_page.postValue(stack.peek())
true
}
}
sealed class Screen {
object SplashScreen : Screen()
object MainScreen : Screen()
object LoginScreen : Screen()
}
sealed class Page {
object HomePage : Page()
data class Detail(val book: BookUio) : Page()
}
}

View file

@ -1,32 +1,48 @@
package com.pixelized.biblib.ui.viewmodel package com.pixelized.biblib.ui.viewmodel.authentication
import android.content.Intent
import android.util.Log import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
import com.google.android.gms.common.api.ApiException
import com.pixelized.biblib.data.network.query.AuthLoginQuery import com.pixelized.biblib.data.network.query.AuthLoginQuery
import com.pixelized.biblib.injection.inject import com.pixelized.biblib.injection.inject
import com.pixelized.biblib.network.client.IBibLibClient import com.pixelized.biblib.network.client.IBibLibClient
import com.pixelized.biblib.repository.credential.ICredentialRepository import com.pixelized.biblib.repository.credential.ICredentialRepository
import com.pixelized.biblib.repository.googlesignin.IGoogleSingInRepository import com.pixelized.biblib.repository.googlesignin.IGoogleSingInRepository
import com.pixelized.biblib.ui.viewmodel.authentication.IAuthentication.State
import com.pixelized.biblib.utils.exception.MissingTokenException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class AuthenticationViewModel : ViewModel() { class AuthenticationViewModel : ViewModel(), IAuthentication {
private val credentialRepository: ICredentialRepository by inject() private val credentialRepository: ICredentialRepository by inject()
private val googleSignIn: IGoogleSingInRepository by inject() private val googleSignIn: IGoogleSingInRepository by inject()
private val client: IBibLibClient by inject() private val client: IBibLibClient by inject()
private var launcher: ActivityResultLauncher<Intent>? = null
private val _state = MutableLiveData<State>(State.Initial)
override val state: LiveData<State> get() = _state
private val _login = MutableLiveData<String>() private val _login = MutableLiveData<String>()
val login: LiveData<String> get() = _login override val login: LiveData<String?> get() = _login
private val _password = MutableLiveData<String>() private val _password = MutableLiveData<String>()
val password: LiveData<String> get() = _password override val password: LiveData<String?> get() = _password
private val _rememberCredential = MutableLiveData<Boolean>() private val _rememberCredential = MutableLiveData<Boolean>()
val rememberCredential: LiveData<Boolean> get() = _rememberCredential override val rememberCredential: LiveData<Boolean> get() = _rememberCredential
init { init {
viewModelScope.launch(Dispatchers.Main) { viewModelScope.launch(Dispatchers.Main) {
@ -36,15 +52,15 @@ class AuthenticationViewModel : ViewModel() {
} }
} }
fun updateLoginField(login: String) { override fun updateLoginField(login: String) {
_login.postValue(login) _login.postValue(login)
} }
fun updatePasswordField(password: String) { override fun updatePasswordField(password: String) {
_password.postValue(password) _password.postValue(password)
} }
fun updateRememberCredential(rememberCredential: Boolean) { override fun updateRememberCredential(rememberCredential: Boolean) {
_rememberCredential.postValue(rememberCredential) _rememberCredential.postValue(rememberCredential)
viewModelScope.launch { viewModelScope.launch {
credentialRepository.rememberCredential = rememberCredential credentialRepository.rememberCredential = rememberCredential
@ -55,11 +71,15 @@ class AuthenticationViewModel : ViewModel() {
} }
} }
fun register() { override fun register() {
viewModelScope.launch {
_state.postValue(State.Loading)
delay(3000)
_state.postValue(State.Initial)
}
} }
fun login() { override fun login() {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
if (rememberCredential.value == true) { if (rememberCredential.value == true) {
credentialRepository.login = login.value credentialRepository.login = login.value
@ -77,11 +97,25 @@ class AuthenticationViewModel : ViewModel() {
} }
@Composable @Composable
fun prepareLoginWithGoogle(): IGoogleSingInRepository.Request = override fun PrepareLoginWithGoogle() {
googleSignIn.prepareLoginWithGoogle() launcher = rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
try {
val task = GoogleSignIn.getSignedInAccountFromIntent(it.data)
val account: GoogleSignInAccount? = task.getResult(ApiException::class.java)
val idToken = account?.idToken ?: throw MissingTokenException()
_state.postValue(State.Connect(idToken))
} catch (exception: Exception) {
_state.postValue(State.Error(exception))
}
}
}
fun loginWithGoogle(request: IGoogleSingInRepository.Request) = override fun loginWithGoogle() {
googleSignIn.loginWithGoogle(request) _state.postValue(State.Loading)
launcher?.launch(googleSignIn.client.signInIntent)
}
companion object { companion object {
const val SHARED_PREF = "BIB_LIB_SHARED_PREF" const val SHARED_PREF = "BIB_LIB_SHARED_PREF"

View file

@ -0,0 +1,48 @@
package com.pixelized.biblib.ui.viewmodel.authentication
import androidx.compose.runtime.Composable
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
interface IAuthentication {
val state: LiveData<State>
val login: LiveData<String?>
val password: LiveData<String?>
val rememberCredential: LiveData<Boolean>
fun updateLoginField(login: String)
fun updatePasswordField(password: String)
fun updateRememberCredential(rememberCredential: Boolean)
fun register()
fun login()
@Composable
fun PrepareLoginWithGoogle()
fun loginWithGoogle()
sealed class State {
object Initial : State()
object Loading : State()
data class Connect(val token: String) : State()
data class Error(val exception: Exception) : State()
}
class Mock(waiting: Boolean = false) : IAuthentication {
override val state: LiveData<State> =
MutableLiveData(if (waiting) State.Loading else State.Initial)
override val login: LiveData<String?> = MutableLiveData("")
override val password: LiveData<String?> = MutableLiveData("")
override val rememberCredential: LiveData<Boolean> = MutableLiveData(true)
override fun updateLoginField(login: String) = Unit
override fun updatePasswordField(password: String) = Unit
override fun updateRememberCredential(rememberCredential: Boolean) = Unit
override fun register() = Unit
override fun login() = Unit
@Composable
override fun PrepareLoginWithGoogle() = Unit
override fun loginWithGoogle() = Unit
}
}

View file

@ -0,0 +1,35 @@
package com.pixelized.biblib.ui.viewmodel.navigation
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.pixelized.biblib.data.ui.BookUio
interface INavigation {
val screen: LiveData<Screen>
val page: LiveData<Page>
fun navigateTo(screen: Screen): Boolean
fun navigateTo(page: Page): Boolean
fun navigateBack(): Boolean
sealed class Screen {
object SplashScreen : Screen()
object MainScreen : Screen()
object LoginScreen : Screen()
}
sealed class Page {
object HomePage : Page()
data class Detail(val book: BookUio) : Page()
}
class Mock(screen: Screen = Screen.SplashScreen, page: Page = Page.HomePage) : INavigation {
override val screen: LiveData<Screen> = MutableLiveData(screen)
override val page: LiveData<Page> = MutableLiveData(page)
override fun navigateTo(screen: Screen): Boolean = false
override fun navigateTo(page: Page): Boolean = false
override fun navigateBack(): Boolean = false
}
}

View file

@ -0,0 +1,41 @@
package com.pixelized.biblib.ui.viewmodel.navigation
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.pixelized.biblib.ui.viewmodel.navigation.INavigation
import com.pixelized.biblib.ui.viewmodel.navigation.INavigation.Page
import com.pixelized.biblib.ui.viewmodel.navigation.INavigation.Screen
import java.util.*
class NavigationViewModel : ViewModel(), INavigation {
private val stack = Stack<Page>()
private val _screen = MutableLiveData<Screen>(Screen.SplashScreen)
override val screen: LiveData<Screen> get() = _screen
private val _page = MutableLiveData<Page>(Page.HomePage)
override val page: LiveData<Page> get() = _page
override fun navigateTo(screen: Screen): Boolean {
_screen.postValue(screen)
return true
}
override fun navigateTo(page: Page): Boolean {
_page.postValue(page)
stack.push(page)
return true
}
override fun navigateBack(): Boolean {
stack.pop()
return if (stack.empty()) {
false
} else {
_page.postValue(stack.peek())
true
}
}
}

View file

@ -0,0 +1,3 @@
package com.pixelized.biblib.utils.alias
fun mock(): Nothing = throw NotImplementedError()