Refactor application loading + Paging on main Page

This commit is contained in:
Thomas Andres Gomez 2021-05-11 23:34:22 +02:00
parent 45f5e9023e
commit 1e58752008
26 changed files with 596 additions and 423 deletions

View file

@ -109,7 +109,7 @@ dependencies {
kapt "androidx.room:room-compiler:2.3.0"
// Paging
implementation "androidx.paging:paging-runtime-ktx:3.0.0"
implementation "androidx.paging:paging-compose:1.0.0-alpha08"
// Test
testImplementation 'junit:junit:4.13.2'

View file

@ -7,8 +7,7 @@
<application
android:name=".BibLibApplication"
android:allowBackup="true"
android:fullBackupOnly="true"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"

View file

@ -1,5 +1,6 @@
package com.pixelized.biblib.database.dao
import androidx.paging.DataSource
import androidx.room.*
import com.pixelized.biblib.database.data.BookDbo
import com.pixelized.biblib.database.relation.BookRelation
@ -10,6 +11,10 @@ interface BookDao {
@Query("SELECT * FROM ${BookDbo.TABLE}")
fun getAll(): List<BookRelation>
@Transaction
@Query("SELECT * FROM ${BookDbo.TABLE}")
fun getBook(): DataSource.Factory<Int, BookRelation>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg books: BookDbo)

View file

@ -9,7 +9,7 @@ import com.pixelized.biblib.database.data.*
data class BookRelation(
@Embedded
val bookDbo: BookDbo,
val book: BookDbo,
@Relation(
parentColumn = BookDbo.ID,
entityColumn = AuthorDbo.ID,
@ -21,15 +21,15 @@ data class BookRelation(
entityColumn = GenreDbo.ID,
associateBy = Junction(BookGenreCrossRef::class)
)
val genres: List<GenreDbo>,
val genres: List<GenreDbo>?,
@Relation(
parentColumn = BookDbo.LANGUAGE_ID,
entityColumn = LanguageDbo.ID
)
val language: LanguageDbo,
val language: LanguageDbo?,
@Relation(
parentColumn = BookDbo.SERIES_ID,
entityColumn = SeriesDbo.ID
)
val series: SeriesDbo,
val series: SeriesDbo?,
)

View file

@ -18,13 +18,9 @@ class BibLibClient : IBibLibClient {
override val service: IBibLibWebServiceAPI = retrofit.create(IBibLibWebServiceAPI::class.java)
// endregion
///////////////////////////////////
// region BibLib webservice Auth
override fun updateBearerToken(token: String) {
interceptor.token = token
}
override var token: String?
get() = interceptor.token
set(value) { interceptor.token = value }
// endregion
}

View file

@ -4,7 +4,7 @@ interface IBibLibClient {
val service: IBibLibWebServiceAPI
fun updateBearerToken(token: String)
var token: String?
companion object {
const val BASE_URL = "https://bib.bibulle.fr"

View file

@ -1,16 +1,27 @@
package com.pixelized.biblib.repository.book
import android.util.Log
import androidx.paging.DataSource
import com.pixelized.biblib.database.BibLibDatabase
import com.pixelized.biblib.database.crossref.BookAuthorCrossRef
import com.pixelized.biblib.database.crossref.BookGenreCrossRef
import com.pixelized.biblib.database.data.*
import com.pixelized.biblib.database.relation.BookRelation
import com.pixelized.biblib.model.*
import com.pixelized.biblib.utils.injection.inject
class BookRepository : IBookRepository {
val database: BibLibDatabase by inject()
override fun update(data: List<Book>) {
override fun getAll(): List<Book> =
database.bookDao().getAll().map { it.toBook() }
override fun getBook(): DataSource.Factory<Int, Book> =
database.bookDao().getBook().map { it.toBook() }
override suspend fun update(data: List<Book>) {
Log.d("pouet", "BookRepository#update(): $data")
val authors = mutableSetOf<AuthorDbo>()
val genres = mutableSetOf<GenreDbo>()
val series = mutableSetOf<SeriesDbo>()
@ -83,4 +94,42 @@ class BookRepository : IBookRepository {
synopsis = synopsis,
isNew = isNew,
)
private fun BookRelation.toBook(): Book = Book(
id = book.id,
title = book.title,
sort = book.sort,
author = authors.map { it.toAuthor() },
haveCover = book.haveCover,
releaseDate = book.releaseDate,
language = language?.toLanguage(),
rating = book.rating,
genre = genres?.map { it.toGenre() },
series = series?.toSeries(),
synopsis = book.synopsis,
isNew = book.isNew,
)
private fun AuthorDbo.toAuthor() = Author(
id = id,
name = name,
sort = sort,
)
private fun LanguageDbo.toLanguage() = Language(
id = id,
code = code,
)
private fun GenreDbo.toGenre() = Genre(
id = id,
name = name,
)
private fun SeriesDbo.toSeries() = Series(
id = id,
name = name,
sort = sort,
index = index,
)
}

View file

@ -1,7 +1,13 @@
package com.pixelized.biblib.repository.book
import androidx.paging.DataSource
import com.pixelized.biblib.model.Book
interface IBookRepository {
fun update(data: List<Book>)
fun getAll(): List<Book>
fun getBook(): DataSource.Factory<Int, Book>
suspend fun update(data: List<Book>)
}

View file

@ -13,7 +13,7 @@ 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.navigation.INavigation.Screen
import com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel.Screen
import com.pixelized.biblib.ui.viewmodel.navigation.NavigationViewModel
class MainActivity : ComponentActivity() {

View file

@ -30,7 +30,7 @@ fun CrossFadeOverlay(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.alpha(.25f)
.alpha(.75f)
.background(Color.Black)
)
// Overlay content.

View file

@ -1,49 +1,56 @@
package com.pixelized.biblib.ui.composable.pages
import android.util.Log
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.items
import com.pixelized.biblib.ui.composable.items.BookThumbnailComposable
import com.pixelized.biblib.ui.data.BookThumbnailUio
import com.pixelized.biblib.ui.theme.BibLibTheme
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
import com.pixelized.biblib.ui.viewmodel.book.IBooksViewModel
import com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel
@Preview
@Composable
fun HomePageComposablePreview() {
BibLibTheme {
val navigation = NavigationViewModel()
HomePageComposable(navigation)
val navigation = INavigationViewModel.Mock()
val books = IBooksViewModel.Mock()
HomePageComposable(navigation, books)
}
}
@Composable
fun HomePageComposable(navigation: INavigation) {
val mock = BookThumbnailMock()
fun HomePageComposable(
navigationViewModel: INavigationViewModel,
booksViewModel: IBooksViewModel
) {
val lazyBooks: LazyPagingItems<BookThumbnailUio> = booksViewModel.books.collectAsLazyPagingItems()
LazyColumn(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
items(mock.bookThumbnails) { thumbnail ->
items(lazyBooks) { thumbnail ->
BookThumbnailComposable(
thumbnail = thumbnail,
thumbnail = thumbnail!!,
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
) { item ->
// TODO:
val bookMock = BookMock().let { it.books[item.id] ?: it.book }
navigation.navigateTo(Detail(bookMock))
// val bookMock = BookMock().let { it.books[item.id] ?: it.book }
// navigation.navigateTo(INavigation.Page.Detail(bookMock))
}
}
}

View file

@ -44,10 +44,12 @@ import com.pixelized.biblib.ui.composable.items.dialog.LoadingCard
import com.pixelized.biblib.ui.composable.items.dialog.SuccessCard
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.ui.viewmodel.authentication.AuthenticationViewModel
import com.pixelized.biblib.ui.viewmodel.authentication.IAuthentication
import com.pixelized.biblib.ui.viewmodel.initialisation.IInitialisation
import com.pixelized.biblib.ui.viewmodel.initialisation.InitialisationViewModel
import com.pixelized.biblib.ui.viewmodel.navigation.INavigation
import com.pixelized.biblib.ui.viewmodel.authentication.IAuthenticationViewModel
import com.pixelized.biblib.ui.viewmodel.book.BooksViewModel
import com.pixelized.biblib.ui.viewmodel.book.IBooksViewModel
import com.pixelized.biblib.ui.viewmodel.credential.CredentialViewModel
import com.pixelized.biblib.ui.viewmodel.credential.ICredentialViewModel
import com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel
import com.pixelized.biblib.ui.viewmodel.navigation.NavigationViewModel
@ -55,26 +57,31 @@ private const val LE_LOAD_BOOK = "LE_LOAD_BOOK"
@Composable
fun LoginScreenComposable(
navigation: INavigation = viewModel<NavigationViewModel>(),
authentication: IAuthentication = viewModel<AuthenticationViewModel>(),
initialisation: IInitialisation = viewModel<InitialisationViewModel>()
navigationViewModel: INavigationViewModel = viewModel<NavigationViewModel>(),
credentialViewModel: ICredentialViewModel = viewModel<CredentialViewModel>(),
authenticationViewModel: IAuthenticationViewModel = viewModel<AuthenticationViewModel>(),
bookViewModel: IBooksViewModel = viewModel<BooksViewModel>()
) {
Box(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
) {
authentication.PrepareLoginWithGoogle()
LoginScreenNavigationComposable(navigation, authentication, initialisation)
LoginScreenContentComposable(authentication)
LoginScreenDialogComposable(authentication, initialisation)
authenticationViewModel.PrepareLoginWithGoogle()
LoginScreenNavigationComposable(
navigationViewModel,
authenticationViewModel,
bookViewModel
)
LoginScreenContentComposable(credentialViewModel, authenticationViewModel)
LoginScreenDialogComposable(authenticationViewModel, bookViewModel)
}
}
@Composable
private fun LoginScreenContentComposable(
authentication: IAuthentication,
credentialViewModel: ICredentialViewModel,
authenticationViewModel: IAuthenticationViewModel,
) {
Column(
modifier = Modifier
@ -86,45 +93,47 @@ private fun LoginScreenContentComposable(
Spacer(modifier = Modifier.weight(1f))
Title()
Spacer(modifier = Modifier.weight(1f))
Form(authentication)
Form(credentialViewModel, authenticationViewModel)
Spacer(modifier = Modifier.weight(2f))
SignIn(authentication)
SignIn(authenticationViewModel)
}
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun LoginScreenDialogComposable(
authentication: IAuthentication,
initialisation: IInitialisation,
authenticationViewModel: IAuthenticationViewModel,
booksViewModel: IBooksViewModel,
) {
val authenticationState = authentication.state.observeAsState()
val bookLoadingState = initialisation.state.observeAsState()
val authState = authenticationViewModel.state.observeAsState()
val bookState = booksViewModel.state.observeAsState()
val visible = authState.value is IAuthenticationViewModel.State.Error
|| bookState.value is IBooksViewModel.State.Error
CrossFadeOverlay(
modifier = Modifier.clickable {
if (authenticationState.value is IAuthentication.State.Error) {
authentication.clearState()
if (visible) {
authenticationViewModel.clearState()
}
},
visible = (authenticationState.value is IAuthentication.State.Initial
&& bookLoadingState.value is IInitialisation.State.Initial).not()
visible = visible
) {
AnimatedVisibility(
modifier = Modifier.align(Alignment.Center),
visible = authenticationState.value is IAuthentication.State.Error,
visible = authState.value is IAuthenticationViewModel.State.Error,
initiallyVisible = false,
enter = expandVertically(Alignment.CenterVertically),
exit = shrinkVertically(Alignment.CenterVertically),
) {
ErrorCard(
message = stringResource(id = R.string.error_generic),
exception = (authenticationState.value as? IAuthentication.State.Error)?.exception
message = stringResource(id = R.string.error_authentication),
exception = (authState.value as? IAuthenticationViewModel.State.Error)?.exception
)
}
AnimatedVisibility(
modifier = Modifier.align(Alignment.Center),
visible = authenticationState.value is IAuthentication.State.Loading,
visible = authState.value is IAuthenticationViewModel.State.Loading,
initiallyVisible = false,
enter = expandVertically(Alignment.CenterVertically),
exit = shrinkVertically(Alignment.CenterVertically),
@ -135,7 +144,8 @@ private fun LoginScreenDialogComposable(
}
AnimatedVisibility(
modifier = Modifier.align(Alignment.Center),
visible = authenticationState.value is IAuthentication.State.Connect,
visible = (authState.value as? IAuthenticationViewModel.State.Finished)?.logged
?: false,
initiallyVisible = false,
enter = expandVertically(Alignment.CenterVertically),
exit = shrinkVertically(Alignment.CenterVertically),
@ -146,19 +156,19 @@ private fun LoginScreenDialogComposable(
}
AnimatedVisibility(
modifier = Modifier.align(Alignment.Center),
visible = bookLoadingState.value is IInitialisation.State.Error,
visible = bookState.value is IBooksViewModel.State.Error,
initiallyVisible = false,
enter = expandVertically(Alignment.CenterVertically),
exit = shrinkVertically(Alignment.CenterVertically),
) {
ErrorCard(
message = stringResource(id = R.string.error_generic),
exception = (bookLoadingState.value as? IInitialisation.State.Error)?.exception
message = stringResource(id = R.string.error_book),
exception = (bookState.value as? IBooksViewModel.State.Error)?.exception
)
}
AnimatedVisibility(
modifier = Modifier.align(Alignment.Center),
visible = bookLoadingState.value is IInitialisation.State.Loading,
visible = bookState.value is IBooksViewModel.State.Loading,
initiallyVisible = false,
enter = expandVertically(Alignment.CenterVertically),
exit = shrinkVertically(Alignment.CenterVertically),
@ -169,7 +179,7 @@ private fun LoginScreenDialogComposable(
}
AnimatedVisibility(
modifier = Modifier.align(Alignment.Center),
visible = bookLoadingState.value is IInitialisation.State.Finished,
visible = bookState.value is IBooksViewModel.State.Finished,
initiallyVisible = false,
enter = expandVertically(Alignment.CenterVertically),
exit = shrinkVertically(Alignment.CenterVertically),
@ -183,22 +193,22 @@ private fun LoginScreenDialogComposable(
@Composable
private fun LoginScreenNavigationComposable(
navigation: INavigation,
authentication: IAuthentication,
initialisation: IInitialisation,
navigationViewModel: INavigationViewModel,
authenticationViewModel: IAuthenticationViewModel,
bookViewModel: IBooksViewModel,
) {
val authenticationState = authentication.state.observeAsState()
val bookLoadingState = initialisation.state.observeAsState()
if (authenticationState.value == IAuthentication.State.Connect) {
LaunchedEffect(LE_LOAD_BOOK) {
initialisation.loadBook()
val authenticationState = authenticationViewModel.state.observeAsState()
(authenticationState.value as? IAuthenticationViewModel.State.Finished)?.let {
if (it.logged) {
LaunchedEffect(LE_LOAD_BOOK) {
bookViewModel.updateBooks()
}
val bookLoadingState = bookViewModel.state.observeAsState()
if (bookLoadingState.value is IBooksViewModel.State.Finished) {
navigationViewModel.navigateTo(INavigationViewModel.Screen.MainScreen)
}
}
}
if (bookLoadingState.value is IInitialisation.State.Finished) {
navigation.navigateTo(INavigation.Screen.MainScreen)
}
}
@Composable
@ -215,12 +225,13 @@ private fun ColumnScope.Title() {
@Composable
private fun ColumnScope.Form(
authentication: IAuthentication,
credentialViewModel: ICredentialViewModel,
authenticationViewModel: IAuthenticationViewModel,
) {
val focusRequester = remember { FocusRequester() }
val localFocus = LocalFocusManager.current
LoginField(
authentication = authentication,
credentialViewModel = credentialViewModel,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
@ -228,7 +239,7 @@ private fun ColumnScope.Form(
keyboardActions = KeyboardActions { focusRequester.requestFocus() }
)
PasswordField(
authentication = authentication,
credentialViewModel = credentialViewModel,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp)
@ -237,7 +248,7 @@ private fun ColumnScope.Form(
keyboardActions = KeyboardActions { localFocus.clearFocus() }
)
CredentialRemember(
authentication = authentication,
credentialViewModel = credentialViewModel,
modifier = Modifier
.height(48.dp)
.padding(bottom = 16.dp)
@ -257,7 +268,12 @@ private fun ColumnScope.Form(
Text(text = stringResource(id = R.string.action_register))
}
Button(onClick = {
authentication.login()
val login = credentialViewModel.login.value
val password = credentialViewModel.password.value
val rememberCredential = credentialViewModel.rememberCredential.value
if (login != null && password != null && rememberCredential != null) {
authenticationViewModel.login(login, password, rememberCredential)
}
}) {
Text(text = stringResource(id = R.string.action_login))
}
@ -266,13 +282,13 @@ private fun ColumnScope.Form(
@Composable
private fun SignIn(
authentication: IAuthentication,
authenticationViewModel: IAuthenticationViewModel,
) {
Button(
modifier = Modifier.fillMaxWidth(),
colors = outlinedButtonColors(),
onClick = {
authentication.loginWithGoogle()
authenticationViewModel.loginWithGoogle()
}) {
Image(
modifier = Modifier.padding(end = 8.dp),
@ -284,16 +300,16 @@ private fun SignIn(
@Composable
private fun LoginField(
authentication: IAuthentication,
credentialViewModel: ICredentialViewModel,
modifier: Modifier = Modifier,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions(),
) {
val login: State<String?> = authentication.login.observeAsState()
val login: State<String?> = credentialViewModel.login.observeAsState()
TextField(
modifier = modifier,
value = login.value ?: "",
onValueChange = { authentication.updateLoginField(it) },
onValueChange = { credentialViewModel.updateLoginField(it) },
label = { Text(text = stringResource(id = R.string.authentication_login)) },
colors = outlinedTextFieldColors(),
maxLines = 1,
@ -305,17 +321,17 @@ private fun LoginField(
@Composable
private fun PasswordField(
authentication: IAuthentication,
credentialViewModel: ICredentialViewModel,
modifier: Modifier = Modifier,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions(),
) {
val password = authentication.password.observeAsState()
val password = credentialViewModel.password.observeAsState()
var passwordVisibility by remember { mutableStateOf(false) }
TextField(
modifier = modifier,
value = password.value ?: "",
onValueChange = { authentication.updatePasswordField(it) },
onValueChange = { credentialViewModel.updatePasswordField(it) },
label = { Text(text = stringResource(id = R.string.authentication_password)) },
colors = outlinedTextFieldColors(),
maxLines = 1,
@ -336,12 +352,12 @@ private fun PasswordField(
@Composable
private fun CredentialRemember(
authentication: IAuthentication,
credentialViewModel: ICredentialViewModel,
modifier: Modifier = Modifier
) {
val credential = authentication.rememberCredential.observeAsState()
val credential = credentialViewModel.rememberCredential.observeAsState()
Row(modifier = modifier.clickable {
authentication.updateRememberCredential(
credentialViewModel.updateRememberCredential(
rememberCredential = credential.value?.not() ?: false
)
}) {
@ -363,9 +379,15 @@ private fun CredentialRemember(
@Composable
fun LoginScreenComposablePreview() {
BibLibTheme {
val navigationViewModel = INavigation.Mock()
val authenticationViewModel = IAuthentication.Mock()
val initialisation = IInitialisation.Mock()
LoginScreenComposable(navigationViewModel, authenticationViewModel, initialisation)
val navigationViewModel = INavigationViewModel.Mock()
val credentialViewModel = ICredentialViewModel.Mock()
val authenticationViewModel = IAuthenticationViewModel.Mock()
val bookViewModel = IBooksViewModel.Mock()
LoginScreenComposable(
navigationViewModel,
credentialViewModel,
authenticationViewModel,
bookViewModel,
)
}
}

View file

@ -1,32 +1,30 @@
package com.pixelized.biblib.ui.composable.screen
import androidx.compose.animation.*
import androidx.compose.foundation.layout.Box
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.sharp.ArrowBack
import androidx.compose.material.icons.sharp.LocalLibrary
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntSize
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.navigation.INavigation
import com.pixelized.biblib.ui.viewmodel.navigation.INavigation.Page
import com.pixelized.biblib.ui.viewmodel.book.BooksViewModel
import com.pixelized.biblib.ui.viewmodel.book.IBooksViewModel
import com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel
import com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel.Page
import com.pixelized.biblib.ui.viewmodel.navigation.NavigationViewModel
@Preview
@Composable
fun ToolbarComposableDarkPreview() {
BibLibTheme(darkTheme = false) {
ToolbarComposable(navigation = INavigation.Mock())
ToolbarComposable(navigationViewModel = INavigationViewModel.Mock())
}
}
@ -34,7 +32,7 @@ fun ToolbarComposableDarkPreview() {
@Composable
fun ToolbarComposableLightPreview() {
BibLibTheme(darkTheme = true) {
ToolbarComposable(navigation = INavigation.Mock())
ToolbarComposable(navigationViewModel = INavigationViewModel.Mock())
}
}
@ -42,23 +40,27 @@ fun ToolbarComposableLightPreview() {
@Composable
fun MainScreenComposablePreview() {
BibLibTheme {
MainScreenComposable(INavigation.Mock(page = Page.HomePage))
MainScreenComposable(
INavigationViewModel.Mock(page = Page.HomePage),
IBooksViewModel.Mock()
)
}
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun MainScreenComposable(
navigation: INavigation = viewModel<NavigationViewModel>()
navigationViewModel: INavigationViewModel = viewModel<NavigationViewModel>(),
booksViewModel: IBooksViewModel = viewModel<BooksViewModel>(),
) {
val page by navigation.page.observeAsState()
val page by navigationViewModel.page.observeAsState()
LaunchedEffect(key1 = "MainScreen", block = {
navigation.navigateTo(Page.HomePage)
navigationViewModel.navigateTo(Page.HomePage)
})
Scaffold(
topBar = { ToolbarComposable(navigation) },
topBar = { ToolbarComposable(navigationViewModel) },
) {
AnimatedVisibility(
visible = page is Page.HomePage,
@ -66,7 +68,7 @@ fun MainScreenComposable(
enter = slideInHorizontally(initialOffsetX = { width -> -width }),
exit = slideOutHorizontally(targetOffsetX = { width -> -width }),
) {
HomePageComposable(navigation)
HomePageComposable(navigationViewModel, booksViewModel)
}
AnimatedVisibility(
visible = page is Page.Detail,
@ -83,16 +85,16 @@ fun MainScreenComposable(
}
@Composable
fun ToolbarComposable(navigation: INavigation) {
fun ToolbarComposable(navigationViewModel: INavigationViewModel) {
TopAppBar(
title = { Text(stringResource(id = R.string.app_name)) },
navigationIcon = { NavigationIcon(navigation) }
navigationIcon = { NavigationIcon(navigationViewModel) }
)
}
@Composable
fun NavigationIcon(navigation: INavigation) {
val page: Page? by navigation.page.observeAsState()
fun NavigationIcon(navigationViewModel: INavigationViewModel) {
val page: Page? by navigationViewModel.page.observeAsState()
Crossfade(targetState = page) {
when (it) {
@ -103,7 +105,7 @@ fun NavigationIcon(navigation: INavigation) {
)
}
else -> IconButton(onClick = {
navigation.navigateBack()
navigationViewModel.navigateBack()
}) {
Icon(
imageVector = Icons.Sharp.ArrowBack,

View file

@ -6,9 +6,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -21,53 +19,93 @@ import com.pixelized.biblib.R
import com.pixelized.biblib.ui.composable.items.dialog.CrossFadeOverlay
import com.pixelized.biblib.ui.composable.items.dialog.ErrorCard
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.ui.viewmodel.initialisation.IInitialisation
import com.pixelized.biblib.ui.viewmodel.initialisation.InitialisationViewModel
import com.pixelized.biblib.ui.viewmodel.navigation.INavigation
import com.pixelized.biblib.ui.viewmodel.authentication.AuthenticationViewModel
import com.pixelized.biblib.ui.viewmodel.authentication.IAuthenticationViewModel
import com.pixelized.biblib.ui.viewmodel.book.BooksViewModel
import com.pixelized.biblib.ui.viewmodel.book.IBooksViewModel
import com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel
import com.pixelized.biblib.ui.viewmodel.navigation.NavigationViewModel
import kotlinx.coroutines.delay
import java.util.*
private const val ANIMATION_DURATION = 1000
private const val LAUNCH_EFFECT_LOAD_APPLICATION = "LoadApplication"
private const val LAUNCH_EFFECT_AUTHENTICATION = "LAUNCH_EFFECT_AUTHENTICATION"
private const val LAUNCH_EFFECT_BOOK = "LAUNCH_EFFECT_BOOK"
private const val LAUNCH_EFFECT_NAVIGATION = "LAUNCH_EFFECT_NAVIGATION"
@Preview
@Composable
fun SplashScreenComposablePreview() {
BibLibTheme {
val initialisation = IInitialisation.Mock(IInitialisation.State.Loading)
val navigation = INavigation.Mock()
SplashScreenComposable(initialisation, navigation, true)
val authenticationViewModel = IAuthenticationViewModel.Mock()
val bookViewModel = IBooksViewModel.Mock()
val navigation = INavigationViewModel.Mock()
SplashScreenComposable(
authenticationViewModel = authenticationViewModel,
bookViewModel = bookViewModel,
navigationViewModel = navigation,
initiallyVisible = true
)
}
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun SplashScreenComposable(
initialisation: IInitialisation = viewModel<InitialisationViewModel>(),
navigation: INavigation = viewModel<NavigationViewModel>(),
authenticationViewModel: IAuthenticationViewModel = viewModel<AuthenticationViewModel>(),
bookViewModel: IBooksViewModel = viewModel<BooksViewModel>(),
navigationViewModel: INavigationViewModel = viewModel<NavigationViewModel>(),
initiallyVisible: Boolean = false,
) {
val state by initialisation.state.observeAsState()
val authenticationState: IAuthenticationViewModel.State? by authenticationViewModel.state.observeAsState()
val bookState by bookViewModel.state.observeAsState()
val contentVisible = remember { mutableStateOf(true) }
LaunchedEffect(LAUNCH_EFFECT_LOAD_APPLICATION) {
initialisation.loadApplication()
// Content
ContentComposable(
visible = contentVisible.value,
initiallyVisible = initiallyVisible
)
// Dialog
AuthenticationError(state = authenticationState as? IAuthenticationViewModel.State.Error)
BookError(state = bookState as? IBooksViewModel.State.Error)
LaunchedEffect(LAUNCH_EFFECT_AUTHENTICATION) {
delay(1500)
authenticationViewModel.autoLogin()
}
ContentComposable(state = state, initiallyVisible = initiallyVisible)
DialogComposable(state = state)
NavigationComposable(navigation = navigation, state = state)
(authenticationState as? IAuthenticationViewModel.State.Finished)?.let {
if (it.logged) {
LaunchedEffect(LAUNCH_EFFECT_BOOK) {
bookViewModel.updateBooks()
}
(bookState as? IBooksViewModel.State.Finished)?.let {
LaunchedEffect(LAUNCH_EFFECT_NAVIGATION) {
contentVisible.value = false
delay(1000)
navigationViewModel.navigateTo(INavigationViewModel.Screen.MainScreen)
}
}
} else {
LaunchedEffect(LAUNCH_EFFECT_NAVIGATION) {
contentVisible.value = false
delay(1000)
navigationViewModel.navigateTo(INavigationViewModel.Screen.LoginScreen)
}
}
}
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun ContentComposable(
state: IInitialisation.State?,
duration: Int = ANIMATION_DURATION,
visible: Boolean = false,
initiallyVisible: Boolean = false,
) {
val typography = MaterialTheme.typography
val visible = state !is IInitialisation.State.Finished
Box(
modifier = Modifier
@ -143,10 +181,10 @@ private fun ContentComposable(
@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun DialogComposable(state: IInitialisation.State?) {
private fun AuthenticationError(state: IAuthenticationViewModel.State.Error?) {
CrossFadeOverlay(
modifier = Modifier.clickable {},
visible = state is IInitialisation.State.Error
visible = state != null
) {
AnimatedVisibility(
modifier = Modifier.align(Alignment.Center),
@ -157,26 +195,30 @@ private fun DialogComposable(state: IInitialisation.State?) {
) {
ErrorCard(
message = stringResource(id = R.string.error_generic),
exception = (state as? IInitialisation.State.Error)?.exception
exception = state?.exception
)
}
}
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun NavigationComposable(
navigation: INavigation,
state: IInitialisation.State?,
duration: Int = ANIMATION_DURATION,
) {
if (state is IInitialisation.State.Finished) {
LaunchedEffect(key1 = "SplashScreen.navigateTo()") {
delay(duration.toLong())
if (state.needLogin) {
navigation.navigateTo(INavigation.Screen.LoginScreen)
} else {
navigation.navigateTo(INavigation.Screen.MainScreen)
}
private fun BookError(state: IBooksViewModel.State.Error?) {
CrossFadeOverlay(
modifier = Modifier.clickable {},
visible = state != null
) {
AnimatedVisibility(
modifier = Modifier.align(Alignment.Center),
visible = true,
initiallyVisible = false,
enter = expandVertically(Alignment.CenterVertically),
exit = shrinkVertically(Alignment.CenterVertically),
) {
ErrorCard(
message = stringResource(id = R.string.error_generic),
exception = state?.exception
)
}
}
}

View file

@ -1,6 +1,7 @@
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
@ -12,91 +13,60 @@ 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.network.data.query.AuthLoginQuery
import com.pixelized.biblib.network.client.IBibLibClient
import com.pixelized.biblib.network.data.query.AuthLoginQuery
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.ui.viewmodel.authentication.IAuthenticationViewModel.State
import com.pixelized.biblib.utils.exception.MissingTokenException
import com.pixelized.biblib.utils.injection.inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class AuthenticationViewModel : ViewModel(), IAuthentication {
class AuthenticationViewModel : ViewModel(), IAuthenticationViewModel {
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>()
override val login: LiveData<String?> get() = _login
private val _password = MutableLiveData<String>()
override val password: LiveData<String?> get() = _password
private val _rememberCredential = MutableLiveData<Boolean>()
override val rememberCredential: LiveData<Boolean> get() = _rememberCredential
init {
viewModelScope.launch(Dispatchers.Main) {
_login.value = credentialRepository.login
_password.value = credentialRepository.password
_rememberCredential.value = credentialRepository.rememberCredential
}
}
override fun updateLoginField(login: String) {
_login.postValue(login)
}
override fun updatePasswordField(password: String) {
_password.postValue(password)
}
override fun updateRememberCredential(rememberCredential: Boolean) {
viewModelScope.launch {
_rememberCredential.postValue(rememberCredential)
credentialRepository.rememberCredential = rememberCredential
if (rememberCredential) {
credentialRepository.login = login.value
credentialRepository.password = password.value
} else {
credentialRepository.login = null
credentialRepository.password = null
}
}
}
override fun clearState() {
_state.postValue(State.Initial)
}
override fun login() {
override fun autoLogin() {
viewModelScope.launch(Dispatchers.IO) {
// TODO : validation !
if (rememberCredential.value == true) {
credentialRepository.login = login.value
credentialRepository.password = password.value
_state.postValue(State.Initial)
_state.postValue(State.Loading)
try {
val logged = autoLoginWithGoogle() || autologinWithCredential()
_state.postValue(State.Finished(logged))
} catch (exception: Exception) {
_state.postValue(State.Error(exception))
}
}
}
override fun login(login: String, password: String, rememberCredential: Boolean) {
viewModelScope.launch(Dispatchers.IO) {
if (rememberCredential) {
credentialRepository.login = login
credentialRepository.password = password
} else {
credentialRepository.login = null
credentialRepository.password = null
}
val query = AuthLoginQuery(
username = login.value,
password = password.value
)
val query = AuthLoginQuery(username = login, password = password)
_state.postValue(State.Loading)
try {
val response = client.service.login(query)
val idToken = response.token ?: throw MissingTokenException()
client.updateBearerToken(idToken)
_state.postValue(State.Connect)
client.token = idToken
_state.postValue(State.Finished(true))
} catch (exception: Exception) {
Log.e("AuthenticationViewModel", exception.message, exception)
_state.postValue(State.Error(exception))
}
}
@ -111,9 +81,10 @@ class AuthenticationViewModel : ViewModel(), IAuthentication {
val task = GoogleSignIn.getSignedInAccountFromIntent(it.data)
val account: GoogleSignInAccount? = task.getResult(ApiException::class.java)
val idToken = account?.idToken ?: throw MissingTokenException()
client.updateBearerToken(idToken)
_state.postValue(State.Connect)
client.token = idToken
_state.postValue(State.Finished(true))
} catch (exception: Exception) {
Log.e("AuthenticationViewModel", exception.message, exception)
_state.postValue(State.Error(exception))
}
}
@ -123,4 +94,46 @@ class AuthenticationViewModel : ViewModel(), IAuthentication {
_state.postValue(State.Loading)
launcher?.launch(googleSignIn.client.signInIntent)
}
private suspend fun autoLoginWithGoogle(): Boolean {
val googleToken = googleSignIn.lastGoogleToken
return if (googleToken != null) {
try {
client.service.loginWithGoogle(googleToken).let { response ->
if (response.token != null) {
client.token = response.token
true
} else {
false
}
}
} catch (e: Exception) {
false
}
} else {
false
}
}
private suspend fun autologinWithCredential(): Boolean {
val login = credentialRepository.login
val password = credentialRepository.password
return if (login != null && password != null) {
try {
val query = AuthLoginQuery(login, password)
client.service.login(query).let { response ->
if (response.token != null) {
client.token = response.token
true
} else {
false
}
}
} catch (e: Exception) {
false
}
} else {
false
}
}
}

View file

@ -1,48 +0,0 @@
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 clearState()
fun login()
@Composable
fun PrepareLoginWithGoogle()
fun loginWithGoogle()
sealed class State {
object Initial : State()
object Loading : State()
object Connect : State()
data class Error(val exception: Exception) : State()
}
class Mock(state: State = State.Initial) : IAuthentication {
override val state: LiveData<State> = MutableLiveData(state)
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 clearState() = Unit
override fun login() = Unit
@Composable
override fun PrepareLoginWithGoogle() = Unit
override fun loginWithGoogle() = Unit
}
}

View file

@ -0,0 +1,38 @@
package com.pixelized.biblib.ui.viewmodel.authentication
import androidx.compose.runtime.Composable
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
interface IAuthenticationViewModel {
val state: LiveData<State>
fun clearState()
fun autoLogin()
fun login(login: String, password: String, rememberCredential: Boolean)
@Composable
fun PrepareLoginWithGoogle()
fun loginWithGoogle()
sealed class State {
object Initial : State()
object Loading : State()
data class Finished(val logged: Boolean) : State()
data class Error(val exception: Exception) : State()
}
class Mock(state: State = State.Initial) : IAuthenticationViewModel {
override val state: LiveData<State> = MutableLiveData(state)
override fun clearState() = Unit
override fun autoLogin() = Unit
override fun login(login: String, password: String, rememberCredential: Boolean) = Unit
@Composable
override fun PrepareLoginWithGoogle() = Unit
override fun loginWithGoogle() = Unit
}
}

View file

@ -0,0 +1,82 @@
package com.pixelized.biblib.ui.viewmodel.book
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import com.pixelized.biblib.model.Book
import com.pixelized.biblib.network.client.IBibLibClient
import com.pixelized.biblib.network.factory.BookFactory
import com.pixelized.biblib.repository.apiCache.IAPICacheRepository
import com.pixelized.biblib.repository.book.IBookRepository
import com.pixelized.biblib.ui.data.BookThumbnailUio
import com.pixelized.biblib.utils.injection.inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
class BooksViewModel : ViewModel(), IBooksViewModel {
private val bookRepository: IBookRepository by inject()
private val client: IBibLibClient by inject()
private val apiCache: IAPICacheRepository by inject()
private val _state = MutableLiveData<IBooksViewModel.State>()
override val state: LiveData<IBooksViewModel.State> get() = _state
private val source
get() = bookRepository.getBook()
.map { it.toThumbnail() }
.asPagingSourceFactory(Dispatchers.Default)
override val books: Flow<PagingData<BookThumbnailUio>> = Pager(
config = PagingConfig(pageSize = 20),
pagingSourceFactory = source
).flow
override fun updateBooks() {
viewModelScope.launch(Dispatchers.IO) {
_state.postValue(IBooksViewModel.State.Initial)
_state.postValue(IBooksViewModel.State.Loading)
try {
val updated = loadNewBooks() && loadAllBooks()
_state.postValue(IBooksViewModel.State.Finished(updated))
} catch (exception: Exception) {
Log.e("BooksViewModel", exception.message, exception)
_state.postValue(IBooksViewModel.State.Error(exception))
}
}
}
private suspend fun loadNewBooks(): Boolean {
val cached = apiCache.new
val updated = client.service.new()
return if (cached != updated) {
apiCache.new = updated
true
} else {
false
}
}
private suspend fun loadAllBooks(): Boolean {
client.service.list().let { response ->
val factory = BookFactory()
val books = response.data?.map { dto -> factory.fromListResponseToBook(dto, false) }
books?.let { data -> bookRepository.update(data) }
}
return true
}
private fun Book.toThumbnail() = BookThumbnailUio(
id = id,
genre = genre?.joinToString { it.name } ?: "",
title = title,
author = author.joinToString { it.name },
date = releaseDate.toString(),
series = series?.name,
)
}

View file

@ -0,0 +1,32 @@
package com.pixelized.biblib.ui.viewmodel.book
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.paging.PagingData
import com.pixelized.biblib.ui.data.BookThumbnailUio
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
interface IBooksViewModel {
val state: LiveData<State>
val books: Flow<PagingData<BookThumbnailUio>>
fun updateBooks()
sealed class State {
object Initial : State()
object Loading : State()
data class Finished(val updated: Boolean) : State()
data class Error(val exception: Exception) : State()
}
class Mock(initial: State = State.Initial) : IBooksViewModel {
override val state: LiveData<State> = MutableLiveData(initial)
override val books: Flow<PagingData<BookThumbnailUio>>
get() = flowOf()
override fun updateBooks() = Unit
}
}

View file

@ -0,0 +1,54 @@
package com.pixelized.biblib.ui.viewmodel.credential
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.biblib.repository.credential.ICredentialRepository
import com.pixelized.biblib.utils.injection.inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class CredentialViewModel : ViewModel(), ICredentialViewModel {
private val credentialRepository: ICredentialRepository by inject()
private val _login = MutableLiveData<String>()
override val login: LiveData<String?> get() = _login
private val _password = MutableLiveData<String>()
override val password: LiveData<String?> get() = _password
private val _rememberCredential = MutableLiveData<Boolean>()
override val rememberCredential: LiveData<Boolean> get() = _rememberCredential
init {
viewModelScope.launch(Dispatchers.Main) {
_login.value = credentialRepository.login
_password.value = credentialRepository.password
_rememberCredential.value = credentialRepository.rememberCredential
}
}
override fun updateLoginField(login: String) {
_login.postValue(login)
}
override fun updatePasswordField(password: String) {
_password.postValue(password)
}
override fun updateRememberCredential(rememberCredential: Boolean) {
viewModelScope.launch {
_rememberCredential.postValue(rememberCredential)
credentialRepository.rememberCredential = rememberCredential
if (rememberCredential) {
credentialRepository.login = login.value
credentialRepository.password = password.value
} else {
credentialRepository.login = null
credentialRepository.password = null
}
}
}
}

View file

@ -0,0 +1,28 @@
package com.pixelized.biblib.ui.viewmodel.credential
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
interface ICredentialViewModel {
val login: LiveData<String?>
val password: LiveData<String?>
val rememberCredential: LiveData<Boolean>
fun updateLoginField(login: String)
fun updatePasswordField(password: String)
fun updateRememberCredential(rememberCredential: Boolean)
// Todo
// fun validateForm()
class Mock : ICredentialViewModel {
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
}
}

View file

@ -1,35 +0,0 @@
package com.pixelized.biblib.ui.viewmodel.initialisation
import androidx.compose.runtime.Stable
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
interface IInitialisation {
val state: LiveData<State>
fun loadApplication()
fun loadBook()
@Stable
sealed class State {
@Stable
object Initial : State()
@Stable
object Loading : State()
@Stable
class Finished(val needLogin: Boolean) : State()
@Stable
class Error(val exception: Exception) : State()
}
class Mock(private val value: State = State.Initial) : IInitialisation {
override val state get() = MutableLiveData(value)
override fun loadApplication() = Unit
override fun loadBook() = Unit
}
}

View file

@ -1,122 +0,0 @@
package com.pixelized.biblib.ui.viewmodel.initialisation
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.biblib.network.client.IBibLibClient
import com.pixelized.biblib.network.data.query.AuthLoginQuery
import com.pixelized.biblib.network.factory.BookFactory
import com.pixelized.biblib.repository.apiCache.IAPICacheRepository
import com.pixelized.biblib.repository.book.IBookRepository
import com.pixelized.biblib.repository.credential.ICredentialRepository
import com.pixelized.biblib.repository.googleSignIn.IGoogleSingInRepository
import com.pixelized.biblib.ui.viewmodel.initialisation.IInitialisation.State.*
import com.pixelized.biblib.utils.injection.inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class InitialisationViewModel : ViewModel(), IInitialisation {
private val credentialRepository: ICredentialRepository by inject()
private val bookRepository: IBookRepository by inject()
private val googleSignIn: IGoogleSingInRepository by inject()
private val client: IBibLibClient by inject()
private val apiCache: IAPICacheRepository by inject()
private val _state = MutableLiveData<IInitialisation.State>()
override val state: LiveData<IInitialisation.State> get() = _state
override fun loadApplication() {
viewModelScope.launch(Dispatchers.IO) {
_state.postValue(Initial)
_state.postValue(Loading)
delay(2000)
try {
val loggedIn = loginWithGoogle() || loginWithCredential()
if (loggedIn) {
loadNewBooks() && loadAllBooks()
}
_state.postValue(Finished(needLogin = loggedIn.not()))
} catch (exception: Exception) {
_state.postValue(Error(exception))
}
}
}
override fun loadBook() {
viewModelScope.launch(Dispatchers.IO) {
_state.postValue(Initial)
_state.postValue(Loading)
try {
loadNewBooks() && loadAllBooks()
_state.postValue(Finished(needLogin = false))
} catch (exception: Exception) {
_state.postValue(Error(exception))
}
}
}
private suspend fun loginWithGoogle(): Boolean {
val googleToken = googleSignIn.lastGoogleToken
return if (googleToken != null) {
try {
client.service.loginWithGoogle(googleToken).let { response ->
if (response.token != null) {
client.updateBearerToken(response.token)
true
} else {
false
}
}
} catch (e: Exception) {
false
}
} else {
false
}
}
private suspend fun loginWithCredential(): Boolean {
val login = credentialRepository.login
val password = credentialRepository.password
return if (login != null && password != null) {
try {
val query = AuthLoginQuery(login, password)
client.service.login(query).let { response ->
if (response.token != null) {
client.updateBearerToken(response.token)
true
} else {
false
}
}
} catch (e: Exception) {
false
}
} else {
false
}
}
private suspend fun loadNewBooks(): Boolean {
val cached = apiCache.new
val updated = client.service.new()
return if (cached != updated) {
apiCache.new = updated
true
} else {
false
}
}
private suspend fun loadAllBooks(): Boolean {
client.service.list().let { response ->
val factory = BookFactory()
val books = response.data?.map { dto -> factory.fromListResponseToBook(dto, false) }
books?.let { data -> bookRepository.update(data) }
}
return true
}
}

View file

@ -4,7 +4,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.pixelized.biblib.ui.data.BookUio
interface INavigation {
interface INavigationViewModel {
val screen: LiveData<Screen>
val page: LiveData<Page>
@ -24,7 +24,7 @@ interface INavigation {
data class Detail(val book: BookUio) : Page()
}
class Mock(screen: Screen = Screen.SplashScreen, page: Page = Page.HomePage) : INavigation {
class Mock(screen: Screen = Screen.SplashScreen, page: Page = Page.HomePage) : INavigationViewModel {
override val screen: LiveData<Screen> = MutableLiveData(screen)
override val page: LiveData<Page> = MutableLiveData(page)

View file

@ -3,12 +3,11 @@ 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 com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel.Page
import com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel.Screen
import java.util.*
class NavigationViewModel : ViewModel(), INavigation {
class NavigationViewModel : ViewModel(), INavigationViewModel {
private val stack = Stack<Page>()

View file

@ -10,8 +10,12 @@
<string name="action_google_sign_in">Sign in with Google</string>
<string name="error_generic">Oops!</string>
<string name="error_authentication">Oops, connection failed!</string>
<string name="error_book">Oops! library download failed!</string>
<string name="loading_authentication">Entering the Imperial Library of Trantor.</string>
<string name="loading_book">Entering the Imperial Library of Trantor.</string>
<string name="loading_book">Downloading the Imperial Library of Trantor.</string>
<string name="success_authentication">Authentication successful</string>
<string name="success_book">Library successfully loaded</string>