ViewModel refactor so Preview work.
This commit is contained in:
parent
45d2fe1336
commit
903d1973c2
16 changed files with 404 additions and 302 deletions
11
.idea/misc.xml
generated
11
.idea/misc.xml
generated
|
|
@ -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" />
|
||||||
|
|
|
||||||
|
|
@ -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 =
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
package com.pixelized.biblib.utils.alias
|
||||||
|
|
||||||
|
fun mock(): Nothing = throw NotImplementedError()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue