ViewModel refactor so Preview work.
This commit is contained in:
		
							parent
							
								
									45d2fe1336
								
							
						
					
					
						commit
						903d1973c2
					
				
					 16 changed files with 404 additions and 302 deletions
				
			
		| 
						 | 
				
			
			@ -4,7 +4,7 @@ import android.app.Application
 | 
			
		|||
import android.content.Context
 | 
			
		||||
import android.content.SharedPreferences
 | 
			
		||||
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 {
 | 
			
		||||
    private val preferences =
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,56 +1,21 @@
 | 
			
		|||
package com.pixelized.biblib.repository.googlesignin
 | 
			
		||||
 | 
			
		||||
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.GoogleSignInAccount
 | 
			
		||||
import com.google.android.gms.auth.api.signin.GoogleSignInClient
 | 
			
		||||
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.utils.exception.MissingTokenException
 | 
			
		||||
 | 
			
		||||
class GoogleSingInRepository(application: Application) : IGoogleSingInRepository {
 | 
			
		||||
 | 
			
		||||
    override val googleSignInOption: GoogleSignInOptions by lazy {
 | 
			
		||||
    override val option: GoogleSignInOptions by lazy {
 | 
			
		||||
        GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
 | 
			
		||||
            .requestIdToken(application.getString(R.string.biblib_server_id))
 | 
			
		||||
            .requestEmail()
 | 
			
		||||
            .build()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override val googleSignIn: GoogleSignInClient by lazy {
 | 
			
		||||
        GoogleSignIn.getClient(application, googleSignInOption)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @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)
 | 
			
		||||
    override val client: GoogleSignInClient by lazy {
 | 
			
		||||
        GoogleSignIn.getClient(application, option)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,30 +1,9 @@
 | 
			
		|||
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.GoogleSignInOptions
 | 
			
		||||
 | 
			
		||||
interface IGoogleSingInRepository {
 | 
			
		||||
    val googleSignInOption: GoogleSignInOptions
 | 
			
		||||
    val googleSignIn: 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()
 | 
			
		||||
    }
 | 
			
		||||
    val option: GoogleSignInOptions
 | 
			
		||||
    val client: GoogleSignInClient
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -9,15 +9,12 @@ import androidx.compose.runtime.Composable
 | 
			
		|||
import androidx.compose.runtime.getValue
 | 
			
		||||
import androidx.compose.runtime.livedata.observeAsState
 | 
			
		||||
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.MainScreenComposable
 | 
			
		||||
import com.pixelized.biblib.ui.composable.screen.SplashScreenComposable
 | 
			
		||||
import com.pixelized.biblib.ui.theme.BibLibTheme
 | 
			
		||||
import com.pixelized.biblib.ui.viewmodel.NavigationViewModel
 | 
			
		||||
import com.pixelized.biblib.ui.viewmodel.NavigationViewModel.Screen
 | 
			
		||||
import com.pixelized.biblib.ui.viewmodel.navigation.NavigationViewModel
 | 
			
		||||
import com.pixelized.biblib.ui.viewmodel.navigation.INavigation.Screen
 | 
			
		||||
 | 
			
		||||
class MainActivity : ComponentActivity() {
 | 
			
		||||
    private val navigationViewModel: NavigationViewModel by viewModels()
 | 
			
		||||
| 
						 | 
				
			
			@ -46,9 +43,9 @@ fun ContentComposable() {
 | 
			
		|||
 | 
			
		||||
    Crossfade(targetState = main) {
 | 
			
		||||
        when (it) {
 | 
			
		||||
            is Screen.SplashScreen -> SplashScreenComposable(viewModel())
 | 
			
		||||
            is Screen.LoginScreen -> LoginScreenComposable(viewModel(), viewModel())
 | 
			
		||||
            is Screen.MainScreen -> MainScreenComposable(viewModel())
 | 
			
		||||
            is Screen.SplashScreen -> SplashScreenComposable()
 | 
			
		||||
            is Screen.LoginScreen -> LoginScreenComposable()
 | 
			
		||||
            is Screen.MainScreen -> MainScreenComposable()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,10 +1,9 @@
 | 
			
		|||
package com.pixelized.biblib.ui.composable.items
 | 
			
		||||
 | 
			
		||||
import androidx.compose.animation.Crossfade
 | 
			
		||||
import androidx.compose.foundation.layout.Box
 | 
			
		||||
import androidx.compose.foundation.layout.Column
 | 
			
		||||
import androidx.compose.foundation.layout.padding
 | 
			
		||||
import androidx.compose.foundation.layout.width
 | 
			
		||||
import androidx.compose.foundation.background
 | 
			
		||||
import androidx.compose.foundation.clickable
 | 
			
		||||
import androidx.compose.foundation.layout.*
 | 
			
		||||
import androidx.compose.material.Card
 | 
			
		||||
import androidx.compose.material.CircularProgressIndicator
 | 
			
		||||
import androidx.compose.material.MaterialTheme
 | 
			
		||||
| 
						 | 
				
			
			@ -12,6 +11,8 @@ import androidx.compose.material.Text
 | 
			
		|||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.ui.Alignment
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.draw.alpha
 | 
			
		||||
import androidx.compose.ui.graphics.Color
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.compose.ui.text.style.TextAlign
 | 
			
		||||
import androidx.compose.ui.tooling.preview.Preview
 | 
			
		||||
| 
						 | 
				
			
			@ -53,25 +54,42 @@ fun WaitingComposable(
 | 
			
		|||
        targetState = visible
 | 
			
		||||
    ) {
 | 
			
		||||
        if (it) {
 | 
			
		||||
            Card(elevation = 8.dp) {
 | 
			
		||||
                Column(
 | 
			
		||||
            Box(
 | 
			
		||||
                modifier = Modifier
 | 
			
		||||
                    .fillMaxWidth()
 | 
			
		||||
                    .fillMaxHeight()
 | 
			
		||||
                    .clickable {  }
 | 
			
		||||
            ) {
 | 
			
		||||
                Box(
 | 
			
		||||
                    modifier = Modifier
 | 
			
		||||
                        .width(200.dp)
 | 
			
		||||
                        .padding(16.dp)
 | 
			
		||||
                        .fillMaxWidth()
 | 
			
		||||
                        .fillMaxHeight()
 | 
			
		||||
                        .alpha(.25f)
 | 
			
		||||
                        .background(Color.Black)
 | 
			
		||||
                )
 | 
			
		||||
                Card(
 | 
			
		||||
                    elevation = 8.dp,
 | 
			
		||||
                    modifier = Modifier.align(Alignment.Center)
 | 
			
		||||
                ) {
 | 
			
		||||
                    CircularProgressIndicator(
 | 
			
		||||
                    Column(
 | 
			
		||||
                        modifier = Modifier
 | 
			
		||||
                            .align(Alignment.CenterHorizontally)
 | 
			
		||||
                            .width(200.dp)
 | 
			
		||||
                            .padding(16.dp)
 | 
			
		||||
                    )
 | 
			
		||||
                    if (message?.isNotEmpty() == true) {
 | 
			
		||||
                        val typography = MaterialTheme.typography
 | 
			
		||||
                        Text(
 | 
			
		||||
                            modifier = Modifier.align(Alignment.CenterHorizontally),
 | 
			
		||||
                            style = typography.body1,
 | 
			
		||||
                            textAlign = TextAlign.Center,
 | 
			
		||||
                            text = message
 | 
			
		||||
                    ) {
 | 
			
		||||
                        CircularProgressIndicator(
 | 
			
		||||
                            modifier = Modifier
 | 
			
		||||
                                .align(Alignment.CenterHorizontally)
 | 
			
		||||
                                .padding(16.dp)
 | 
			
		||||
                        )
 | 
			
		||||
                        if (message?.isNotEmpty() == true) {
 | 
			
		||||
                            val typography = MaterialTheme.typography
 | 
			
		||||
                            Text(
 | 
			
		||||
                                modifier = Modifier.align(Alignment.CenterHorizontally),
 | 
			
		||||
                                style = typography.body1,
 | 
			
		||||
                                textAlign = TextAlign.Center,
 | 
			
		||||
                                text = message
 | 
			
		||||
                            )
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,8 +12,9 @@ import androidx.compose.ui.tooling.preview.Preview
 | 
			
		|||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import com.pixelized.biblib.ui.composable.items.BookThumbnailComposable
 | 
			
		||||
import com.pixelized.biblib.ui.theme.BibLibTheme
 | 
			
		||||
import com.pixelized.biblib.ui.viewmodel.NavigationViewModel
 | 
			
		||||
import com.pixelized.biblib.ui.viewmodel.NavigationViewModel.Page.Detail
 | 
			
		||||
import com.pixelized.biblib.ui.viewmodel.navigation.INavigation
 | 
			
		||||
import com.pixelized.biblib.ui.viewmodel.navigation.INavigation.Page.Detail
 | 
			
		||||
import com.pixelized.biblib.ui.viewmodel.navigation.NavigationViewModel
 | 
			
		||||
import com.pixelized.biblib.utils.mock.BookMock
 | 
			
		||||
import com.pixelized.biblib.utils.mock.BookThumbnailMock
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -21,12 +22,13 @@ import com.pixelized.biblib.utils.mock.BookThumbnailMock
 | 
			
		|||
@Composable
 | 
			
		||||
fun HomePageComposablePreview() {
 | 
			
		||||
    BibLibTheme {
 | 
			
		||||
        HomePageComposable(null)
 | 
			
		||||
        val navigation = NavigationViewModel()
 | 
			
		||||
        HomePageComposable(navigation)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun HomePageComposable(navigationViewModel: NavigationViewModel?) {
 | 
			
		||||
fun HomePageComposable(navigation: INavigation) {
 | 
			
		||||
    val mock = BookThumbnailMock()
 | 
			
		||||
    LazyColumn(
 | 
			
		||||
        contentPadding = PaddingValues(16.dp),
 | 
			
		||||
| 
						 | 
				
			
			@ -41,7 +43,7 @@ fun HomePageComposable(navigationViewModel: NavigationViewModel?) {
 | 
			
		|||
            ) { item ->
 | 
			
		||||
                // TODO:
 | 
			
		||||
                val bookMock = BookMock().let { it.books[item.id] ?: it.book }
 | 
			
		||||
                navigationViewModel?.navigateTo(Detail(bookMock))
 | 
			
		||||
                navigation.navigateTo(Detail(bookMock))
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,6 @@
 | 
			
		|||
package com.pixelized.biblib.ui.composable.screen
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
import androidx.compose.animation.ExperimentalAnimationApi
 | 
			
		||||
import androidx.compose.foundation.Image
 | 
			
		||||
import androidx.compose.foundation.clickable
 | 
			
		||||
import androidx.compose.foundation.layout.*
 | 
			
		||||
| 
						 | 
				
			
			@ -29,142 +28,140 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation
 | 
			
		|||
import androidx.compose.ui.text.input.VisualTransformation
 | 
			
		||||
import androidx.compose.ui.tooling.preview.Preview
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import androidx.lifecycle.viewmodel.compose.viewModel
 | 
			
		||||
import com.pixelized.biblib.R
 | 
			
		||||
import com.pixelized.biblib.repository.googlesignin.IGoogleSingInRepository.AuthenticationState.Loading
 | 
			
		||||
import com.pixelized.biblib.ui.composable.items.WaitingComposable
 | 
			
		||||
import com.pixelized.biblib.ui.theme.BibLibTheme
 | 
			
		||||
import com.pixelized.biblib.ui.viewmodel.AuthenticationViewModel
 | 
			
		||||
import com.pixelized.biblib.ui.viewmodel.NavigationViewModel
 | 
			
		||||
import com.pixelized.biblib.ui.viewmodel.authentication.AuthenticationViewModel
 | 
			
		||||
import com.pixelized.biblib.ui.viewmodel.authentication.IAuthentication
 | 
			
		||||
import com.pixelized.biblib.ui.viewmodel.navigation.INavigation
 | 
			
		||||
import com.pixelized.biblib.ui.viewmodel.navigation.NavigationViewModel
 | 
			
		||||
 | 
			
		||||
@Preview
 | 
			
		||||
@Composable
 | 
			
		||||
fun LoginScreenComposablePreview() {
 | 
			
		||||
    BibLibTheme {
 | 
			
		||||
        val navigationViewModel = NavigationViewModel()
 | 
			
		||||
        val authenticationViewModel = AuthenticationViewModel()
 | 
			
		||||
        LoginScreenComposable(navigationViewModel, authenticationViewModel)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@OptIn(ExperimentalAnimationApi::class)
 | 
			
		||||
@Composable
 | 
			
		||||
fun LoginScreenComposable(
 | 
			
		||||
    navigationViewModel: NavigationViewModel,
 | 
			
		||||
    authenticationViewModel: AuthenticationViewModel,
 | 
			
		||||
    navigation: INavigation = viewModel<NavigationViewModel>(),
 | 
			
		||||
    authentication: IAuthentication = viewModel<AuthenticationViewModel>(),
 | 
			
		||||
) {
 | 
			
		||||
    Box(
 | 
			
		||||
        modifier = Modifier
 | 
			
		||||
            .fillMaxWidth()
 | 
			
		||||
            .fillMaxHeight()
 | 
			
		||||
    ) {
 | 
			
		||||
        val loginWithGoogleRequest = authenticationViewModel.prepareLoginWithGoogle()
 | 
			
		||||
        val typography = MaterialTheme.typography
 | 
			
		||||
 | 
			
		||||
        Column(
 | 
			
		||||
            modifier = Modifier
 | 
			
		||||
                .fillMaxWidth()
 | 
			
		||||
                .fillMaxHeight()
 | 
			
		||||
                .verticalScroll(rememberScrollState())
 | 
			
		||||
                .padding(16.dp)
 | 
			
		||||
        ) {
 | 
			
		||||
            Spacer(modifier = Modifier.weight(1f))
 | 
			
		||||
 | 
			
		||||
            Text(
 | 
			
		||||
                modifier = Modifier
 | 
			
		||||
                    .padding(vertical = 16.dp)
 | 
			
		||||
                    .align(alignment = Alignment.CenterHorizontally),
 | 
			
		||||
                style = typography.h4,
 | 
			
		||||
                text = stringResource(id = R.string.welcome_sign_in)
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            Spacer(modifier = Modifier.weight(1f))
 | 
			
		||||
 | 
			
		||||
            val focusRequester = remember { FocusRequester() }
 | 
			
		||||
            val localFocus = LocalFocusManager.current
 | 
			
		||||
            LoginField(
 | 
			
		||||
                viewModel = authenticationViewModel,
 | 
			
		||||
                modifier = Modifier
 | 
			
		||||
                    .fillMaxWidth()
 | 
			
		||||
                    .padding(bottom = 16.dp),
 | 
			
		||||
                keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
 | 
			
		||||
                keyboardActions = KeyboardActions { focusRequester.requestFocus() }
 | 
			
		||||
            )
 | 
			
		||||
            PasswordField(
 | 
			
		||||
                viewModel = authenticationViewModel,
 | 
			
		||||
                modifier = Modifier
 | 
			
		||||
                    .fillMaxWidth()
 | 
			
		||||
                    .padding(bottom = 16.dp)
 | 
			
		||||
                    .focusRequester(focusRequester),
 | 
			
		||||
                keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
 | 
			
		||||
                keyboardActions = KeyboardActions { localFocus.clearFocus() }
 | 
			
		||||
            )
 | 
			
		||||
            CredentialRemember(
 | 
			
		||||
                viewModel = authenticationViewModel,
 | 
			
		||||
                modifier = Modifier
 | 
			
		||||
                    .height(48.dp)
 | 
			
		||||
                    .padding(bottom = 16.dp)
 | 
			
		||||
            )
 | 
			
		||||
            Row(
 | 
			
		||||
                modifier = Modifier
 | 
			
		||||
                    .padding(bottom = 16.dp)
 | 
			
		||||
                    .align(Alignment.End)
 | 
			
		||||
            ) {
 | 
			
		||||
                Button(
 | 
			
		||||
                    modifier = Modifier.padding(end = 8.dp),
 | 
			
		||||
                    colors = outlinedButtonColors(),
 | 
			
		||||
                    onClick = {
 | 
			
		||||
                        authenticationViewModel.register()
 | 
			
		||||
                    }) {
 | 
			
		||||
                    Text(text = stringResource(id = R.string.action_register))
 | 
			
		||||
                }
 | 
			
		||||
                Button(onClick = {
 | 
			
		||||
                    authenticationViewModel.login()
 | 
			
		||||
                }) {
 | 
			
		||||
                    Text(text = stringResource(id = R.string.action_login))
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Spacer(modifier = Modifier.weight(2f))
 | 
			
		||||
 | 
			
		||||
            Button(
 | 
			
		||||
                modifier = Modifier.fillMaxWidth(),
 | 
			
		||||
                colors = outlinedButtonColors(),
 | 
			
		||||
                onClick = {
 | 
			
		||||
                    authenticationViewModel.loginWithGoogle(loginWithGoogleRequest)
 | 
			
		||||
                }) {
 | 
			
		||||
                Image(
 | 
			
		||||
                    modifier = Modifier.padding(end = 8.dp),
 | 
			
		||||
                    painter = painterResource(id = R.drawable.ic_google), contentDescription = ""
 | 
			
		||||
                )
 | 
			
		||||
                Text(text = stringResource(id = R.string.action_google_sign_in))
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var waiting: Boolean by remember { mutableStateOf(false) }
 | 
			
		||||
        waiting = when (loginWithGoogleRequest.result.value) {
 | 
			
		||||
            is Loading -> true
 | 
			
		||||
            else -> false
 | 
			
		||||
        }
 | 
			
		||||
        WaitingComposable(
 | 
			
		||||
            modifier = Modifier.align(Alignment.Center),
 | 
			
		||||
            visible = waiting,
 | 
			
		||||
            message = stringResource(id = R.string.loading)
 | 
			
		||||
        )
 | 
			
		||||
        authentication.PrepareLoginWithGoogle()
 | 
			
		||||
        LoginScreenContentComposable(navigation, authentication)
 | 
			
		||||
        LoginScreenWaitingComposable(authentication)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
private fun LoginScreenContentComposable(
 | 
			
		||||
    navigation: INavigation,
 | 
			
		||||
    authentication: IAuthentication,
 | 
			
		||||
) {
 | 
			
		||||
    val typography = MaterialTheme.typography
 | 
			
		||||
    Column(
 | 
			
		||||
        modifier = Modifier
 | 
			
		||||
            .fillMaxWidth()
 | 
			
		||||
            .fillMaxHeight()
 | 
			
		||||
            .verticalScroll(rememberScrollState())
 | 
			
		||||
            .padding(16.dp)
 | 
			
		||||
    ) {
 | 
			
		||||
        Spacer(modifier = Modifier.weight(1f))
 | 
			
		||||
 | 
			
		||||
        Text(
 | 
			
		||||
            modifier = Modifier
 | 
			
		||||
                .padding(vertical = 16.dp)
 | 
			
		||||
                .align(alignment = Alignment.CenterHorizontally),
 | 
			
		||||
            style = typography.h4,
 | 
			
		||||
            text = stringResource(id = R.string.welcome_sign_in)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        Spacer(modifier = Modifier.weight(1f))
 | 
			
		||||
 | 
			
		||||
        val focusRequester = remember { FocusRequester() }
 | 
			
		||||
        val localFocus = LocalFocusManager.current
 | 
			
		||||
        LoginField(
 | 
			
		||||
            authentication = authentication,
 | 
			
		||||
            modifier = Modifier
 | 
			
		||||
                .fillMaxWidth()
 | 
			
		||||
                .padding(bottom = 16.dp),
 | 
			
		||||
            keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
 | 
			
		||||
            keyboardActions = KeyboardActions { focusRequester.requestFocus() }
 | 
			
		||||
        )
 | 
			
		||||
        PasswordField(
 | 
			
		||||
            authentication = authentication,
 | 
			
		||||
            modifier = Modifier
 | 
			
		||||
                .fillMaxWidth()
 | 
			
		||||
                .padding(bottom = 16.dp)
 | 
			
		||||
                .focusRequester(focusRequester),
 | 
			
		||||
            keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
 | 
			
		||||
            keyboardActions = KeyboardActions { localFocus.clearFocus() }
 | 
			
		||||
        )
 | 
			
		||||
        CredentialRemember(
 | 
			
		||||
            authentication = authentication,
 | 
			
		||||
            modifier = Modifier
 | 
			
		||||
                .height(48.dp)
 | 
			
		||||
                .padding(bottom = 16.dp)
 | 
			
		||||
        )
 | 
			
		||||
        Row(
 | 
			
		||||
            modifier = Modifier
 | 
			
		||||
                .padding(bottom = 16.dp)
 | 
			
		||||
                .align(Alignment.End)
 | 
			
		||||
        ) {
 | 
			
		||||
            Button(
 | 
			
		||||
                modifier = Modifier.padding(end = 8.dp),
 | 
			
		||||
                colors = outlinedButtonColors(),
 | 
			
		||||
                onClick = {
 | 
			
		||||
                    authentication.register()
 | 
			
		||||
                }) {
 | 
			
		||||
                Text(text = stringResource(id = R.string.action_register))
 | 
			
		||||
            }
 | 
			
		||||
            Button(onClick = {
 | 
			
		||||
                authentication.login()
 | 
			
		||||
            }) {
 | 
			
		||||
                Text(text = stringResource(id = R.string.action_login))
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Spacer(modifier = Modifier.weight(2f))
 | 
			
		||||
 | 
			
		||||
        Button(
 | 
			
		||||
            modifier = Modifier.fillMaxWidth(),
 | 
			
		||||
            colors = outlinedButtonColors(),
 | 
			
		||||
            onClick = {
 | 
			
		||||
                authentication.loginWithGoogle()
 | 
			
		||||
            }) {
 | 
			
		||||
            Image(
 | 
			
		||||
                modifier = Modifier.padding(end = 8.dp),
 | 
			
		||||
                painter = painterResource(id = R.drawable.ic_google), contentDescription = ""
 | 
			
		||||
            )
 | 
			
		||||
            Text(text = stringResource(id = R.string.action_google_sign_in))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
private fun LoginScreenWaitingComposable(authentication: IAuthentication) {
 | 
			
		||||
    val state = authentication.state.observeAsState()
 | 
			
		||||
    WaitingComposable(
 | 
			
		||||
        visible = state.value is IAuthentication.State.Loading,
 | 
			
		||||
        message = stringResource(id = R.string.loading)
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
private fun LoginField(
 | 
			
		||||
    viewModel: AuthenticationViewModel,
 | 
			
		||||
    authentication: IAuthentication,
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
 | 
			
		||||
    keyboardActions: KeyboardActions = KeyboardActions(),
 | 
			
		||||
) {
 | 
			
		||||
    val login: State<String?> = viewModel.login.observeAsState()
 | 
			
		||||
    val login: State<String?> = authentication.login.observeAsState()
 | 
			
		||||
    TextField(
 | 
			
		||||
        modifier = modifier,
 | 
			
		||||
        value = login.value ?: "",
 | 
			
		||||
        onValueChange = { viewModel.updateLoginField(it) },
 | 
			
		||||
        onValueChange = { authentication.updateLoginField(it) },
 | 
			
		||||
        label = { Text(text = stringResource(id = R.string.authentication_login)) },
 | 
			
		||||
        colors = outlinedTextFieldColors(),
 | 
			
		||||
        maxLines = 1,
 | 
			
		||||
| 
						 | 
				
			
			@ -176,17 +173,17 @@ private fun LoginField(
 | 
			
		|||
 | 
			
		||||
@Composable
 | 
			
		||||
private fun PasswordField(
 | 
			
		||||
    viewModel: AuthenticationViewModel,
 | 
			
		||||
    authentication: IAuthentication,
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
 | 
			
		||||
    keyboardActions: KeyboardActions = KeyboardActions(),
 | 
			
		||||
) {
 | 
			
		||||
    val password = viewModel.password.observeAsState()
 | 
			
		||||
    val password = authentication.password.observeAsState()
 | 
			
		||||
    var passwordVisibility by remember { mutableStateOf(false) }
 | 
			
		||||
    TextField(
 | 
			
		||||
        modifier = modifier,
 | 
			
		||||
        value = password.value ?: "",
 | 
			
		||||
        onValueChange = { viewModel.updatePasswordField(it) },
 | 
			
		||||
        onValueChange = { authentication.updatePasswordField(it) },
 | 
			
		||||
        label = { Text(text = stringResource(id = R.string.authentication_password)) },
 | 
			
		||||
        colors = outlinedTextFieldColors(),
 | 
			
		||||
        maxLines = 1,
 | 
			
		||||
| 
						 | 
				
			
			@ -206,10 +203,15 @@ private fun PasswordField(
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
private fun CredentialRemember(viewModel: AuthenticationViewModel, modifier: Modifier = Modifier) {
 | 
			
		||||
    val credential = viewModel.rememberCredential.observeAsState()
 | 
			
		||||
private fun CredentialRemember(
 | 
			
		||||
    authentication: IAuthentication,
 | 
			
		||||
    modifier: Modifier = Modifier
 | 
			
		||||
) {
 | 
			
		||||
    val credential = authentication.rememberCredential.observeAsState()
 | 
			
		||||
    Row(modifier = modifier.clickable {
 | 
			
		||||
        viewModel.updateRememberCredential(rememberCredential = credential.value?.not() ?: false)
 | 
			
		||||
        authentication.updateRememberCredential(
 | 
			
		||||
            rememberCredential = credential.value?.not() ?: false
 | 
			
		||||
        )
 | 
			
		||||
    }) {
 | 
			
		||||
        Checkbox(
 | 
			
		||||
            modifier = Modifier.align(Alignment.CenterVertically),
 | 
			
		||||
| 
						 | 
				
			
			@ -223,4 +225,24 @@ private fun CredentialRemember(viewModel: AuthenticationViewModel, modifier: Mod
 | 
			
		|||
            text = stringResource(id = R.string.authentication_credential_remember)
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Preview
 | 
			
		||||
@Composable
 | 
			
		||||
fun LoginScreenComposablePreview() {
 | 
			
		||||
    BibLibTheme {
 | 
			
		||||
        val navigationViewModel = INavigation.Mock()
 | 
			
		||||
        val authenticationViewModel = IAuthentication.Mock()
 | 
			
		||||
        LoginScreenComposable(navigationViewModel, authenticationViewModel)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Preview
 | 
			
		||||
@Composable
 | 
			
		||||
fun LoginScreenComposableWaitingPreview() {
 | 
			
		||||
    BibLibTheme {
 | 
			
		||||
        val navigationViewModel = INavigation.Mock()
 | 
			
		||||
        val authenticationViewModel = IAuthentication.Mock(true)
 | 
			
		||||
        LoginScreenComposable(navigationViewModel, authenticationViewModel)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -11,28 +11,28 @@ import androidx.compose.runtime.getValue
 | 
			
		|||
import androidx.compose.runtime.livedata.observeAsState
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.compose.ui.tooling.preview.Preview
 | 
			
		||||
import androidx.lifecycle.viewmodel.compose.viewModel
 | 
			
		||||
import com.pixelized.biblib.R
 | 
			
		||||
import com.pixelized.biblib.ui.composable.pages.DetailPageComposable
 | 
			
		||||
import com.pixelized.biblib.ui.composable.pages.HomePageComposable
 | 
			
		||||
import com.pixelized.biblib.ui.theme.BibLibTheme
 | 
			
		||||
import com.pixelized.biblib.ui.viewmodel.NavigationViewModel
 | 
			
		||||
import com.pixelized.biblib.ui.viewmodel.NavigationViewModel.Page
 | 
			
		||||
import com.pixelized.biblib.ui.viewmodel.navigation.INavigation
 | 
			
		||||
import com.pixelized.biblib.ui.viewmodel.navigation.INavigation.Page
 | 
			
		||||
import com.pixelized.biblib.ui.viewmodel.navigation.NavigationViewModel
 | 
			
		||||
 | 
			
		||||
@Preview
 | 
			
		||||
@Composable
 | 
			
		||||
fun ToolbarComposableDarkPreview() {
 | 
			
		||||
    val viewModel = NavigationViewModel()
 | 
			
		||||
    BibLibTheme(darkTheme = false) {
 | 
			
		||||
        ToolbarComposable(navigationViewModel = viewModel)
 | 
			
		||||
        ToolbarComposable(navigation = INavigation.Mock())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Preview
 | 
			
		||||
@Composable
 | 
			
		||||
fun ToolbarComposableLightPreview() {
 | 
			
		||||
    val viewModel = NavigationViewModel()
 | 
			
		||||
    BibLibTheme(darkTheme = true) {
 | 
			
		||||
        ToolbarComposable(navigationViewModel = viewModel)
 | 
			
		||||
        ToolbarComposable(navigation = INavigation.Mock())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -40,27 +40,26 @@ fun ToolbarComposableLightPreview() {
 | 
			
		|||
@Composable
 | 
			
		||||
fun MainScreenComposablePreview() {
 | 
			
		||||
    BibLibTheme {
 | 
			
		||||
        val viewModel = NavigationViewModel()
 | 
			
		||||
        MainScreenComposable(viewModel)
 | 
			
		||||
        MainScreenComposable(INavigation.Mock(page = Page.HomePage))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun MainScreenComposable(
 | 
			
		||||
    navigationViewModel: NavigationViewModel
 | 
			
		||||
    navigation: INavigation = viewModel<NavigationViewModel>()
 | 
			
		||||
) {
 | 
			
		||||
    val page by navigationViewModel.page.observeAsState()
 | 
			
		||||
    val page by navigation.page.observeAsState()
 | 
			
		||||
 | 
			
		||||
    LaunchedEffect(key1 = "MainScreen", block = {
 | 
			
		||||
        navigationViewModel.navigateTo(Page.HomePage)
 | 
			
		||||
        navigation.navigateTo(Page.HomePage)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    Scaffold(
 | 
			
		||||
        topBar = { ToolbarComposable(navigationViewModel) },
 | 
			
		||||
        topBar = { ToolbarComposable(navigation) },
 | 
			
		||||
    ) {
 | 
			
		||||
        Crossfade(targetState = page) {
 | 
			
		||||
            when (it) {
 | 
			
		||||
                is Page.HomePage -> HomePageComposable(navigationViewModel)
 | 
			
		||||
                is Page.HomePage -> HomePageComposable(navigation)
 | 
			
		||||
                is Page.Detail -> DetailPageComposable(it.book)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -68,16 +67,16 @@ fun MainScreenComposable(
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun ToolbarComposable(navigationViewModel: NavigationViewModel) {
 | 
			
		||||
fun ToolbarComposable(navigation: INavigation) {
 | 
			
		||||
    TopAppBar(
 | 
			
		||||
        title = { Text(stringResource(id = R.string.app_name)) },
 | 
			
		||||
        navigationIcon = { NavigationIcon(navigationViewModel) }
 | 
			
		||||
        navigationIcon = { NavigationIcon(navigation) }
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun NavigationIcon(navigationViewModel: NavigationViewModel) {
 | 
			
		||||
    val page: Page? by navigationViewModel.page.observeAsState()
 | 
			
		||||
fun NavigationIcon(navigation: INavigation) {
 | 
			
		||||
    val page: Page? by navigation.page.observeAsState()
 | 
			
		||||
 | 
			
		||||
    Crossfade(targetState = page) {
 | 
			
		||||
        when (it) {
 | 
			
		||||
| 
						 | 
				
			
			@ -88,7 +87,7 @@ fun NavigationIcon(navigationViewModel: NavigationViewModel) {
 | 
			
		|||
                )
 | 
			
		||||
            }
 | 
			
		||||
            else -> IconButton(onClick = {
 | 
			
		||||
                navigationViewModel.navigateBack()
 | 
			
		||||
                navigation.navigateBack()
 | 
			
		||||
            }) {
 | 
			
		||||
                Icon(
 | 
			
		||||
                    imageVector = Icons.Sharp.ArrowBack,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,8 +11,10 @@ import androidx.compose.runtime.rememberCoroutineScope
 | 
			
		|||
import androidx.compose.ui.Alignment
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.tooling.preview.Preview
 | 
			
		||||
import androidx.lifecycle.viewmodel.compose.viewModel
 | 
			
		||||
import com.pixelized.biblib.ui.theme.BibLibTheme
 | 
			
		||||
import com.pixelized.biblib.ui.viewmodel.NavigationViewModel
 | 
			
		||||
import com.pixelized.biblib.ui.viewmodel.navigation.INavigation
 | 
			
		||||
import com.pixelized.biblib.ui.viewmodel.navigation.NavigationViewModel
 | 
			
		||||
import kotlinx.coroutines.delay
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -21,14 +23,13 @@ import kotlinx.coroutines.launch
 | 
			
		|||
@Composable
 | 
			
		||||
fun SplashScreenComposablePreview() {
 | 
			
		||||
    BibLibTheme {
 | 
			
		||||
        val viewModel = NavigationViewModel()
 | 
			
		||||
        SplashScreenComposable(viewModel)
 | 
			
		||||
        SplashScreenComposable(INavigation.Mock())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun SplashScreenComposable(
 | 
			
		||||
    navigationViewModel: NavigationViewModel
 | 
			
		||||
    navigation: INavigation = viewModel<NavigationViewModel>()
 | 
			
		||||
) {
 | 
			
		||||
    val typography = MaterialTheme.typography
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -48,7 +49,7 @@ fun SplashScreenComposable(
 | 
			
		|||
    LaunchedEffect(key1 = "loading", block = {
 | 
			
		||||
        coroutineScope.launch {
 | 
			
		||||
            delay(1000)
 | 
			
		||||
            navigationViewModel.navigateTo(NavigationViewModel.Screen.LoginScreen)
 | 
			
		||||
            navigation.navigateTo(INavigation.Screen.LoginScreen)
 | 
			
		||||
        }
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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 androidx.activity.compose.rememberLauncherForActivityResult
 | 
			
		||||
import androidx.activity.result.ActivityResultLauncher
 | 
			
		||||
import androidx.activity.result.contract.ActivityResultContracts
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.lifecycle.LiveData
 | 
			
		||||
import androidx.lifecycle.MutableLiveData
 | 
			
		||||
import androidx.lifecycle.ViewModel
 | 
			
		||||
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.injection.inject
 | 
			
		||||
import com.pixelized.biblib.network.client.IBibLibClient
 | 
			
		||||
import com.pixelized.biblib.repository.credential.ICredentialRepository
 | 
			
		||||
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.delay
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
 | 
			
		||||
class AuthenticationViewModel : ViewModel() {
 | 
			
		||||
class AuthenticationViewModel : ViewModel(), IAuthentication {
 | 
			
		||||
 | 
			
		||||
    private val credentialRepository: ICredentialRepository by inject()
 | 
			
		||||
    private val googleSignIn: IGoogleSingInRepository 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>()
 | 
			
		||||
    val login: LiveData<String> get() = _login
 | 
			
		||||
    override val login: LiveData<String?> get() = _login
 | 
			
		||||
 | 
			
		||||
    private val _password = MutableLiveData<String>()
 | 
			
		||||
    val password: LiveData<String> get() = _password
 | 
			
		||||
    override val password: LiveData<String?> get() = _password
 | 
			
		||||
 | 
			
		||||
    private val _rememberCredential = MutableLiveData<Boolean>()
 | 
			
		||||
    val rememberCredential: LiveData<Boolean> get() = _rememberCredential
 | 
			
		||||
    override val rememberCredential: LiveData<Boolean> get() = _rememberCredential
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        viewModelScope.launch(Dispatchers.Main) {
 | 
			
		||||
| 
						 | 
				
			
			@ -36,15 +52,15 @@ class AuthenticationViewModel : ViewModel() {
 | 
			
		|||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun updateLoginField(login: String) {
 | 
			
		||||
    override fun updateLoginField(login: String) {
 | 
			
		||||
        _login.postValue(login)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun updatePasswordField(password: String) {
 | 
			
		||||
    override fun updatePasswordField(password: String) {
 | 
			
		||||
        _password.postValue(password)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun updateRememberCredential(rememberCredential: Boolean) {
 | 
			
		||||
    override fun updateRememberCredential(rememberCredential: Boolean) {
 | 
			
		||||
        _rememberCredential.postValue(rememberCredential)
 | 
			
		||||
        viewModelScope.launch {
 | 
			
		||||
            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) {
 | 
			
		||||
            if (rememberCredential.value == true) {
 | 
			
		||||
                credentialRepository.login = login.value
 | 
			
		||||
| 
						 | 
				
			
			@ -77,11 +97,25 @@ class AuthenticationViewModel : ViewModel() {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    @Composable
 | 
			
		||||
    fun prepareLoginWithGoogle(): IGoogleSingInRepository.Request =
 | 
			
		||||
        googleSignIn.prepareLoginWithGoogle()
 | 
			
		||||
    override fun 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) =
 | 
			
		||||
        googleSignIn.loginWithGoogle(request)
 | 
			
		||||
    override fun loginWithGoogle() {
 | 
			
		||||
        _state.postValue(State.Loading)
 | 
			
		||||
        launcher?.launch(googleSignIn.client.signInIntent)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        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