Fix lazy detail loading.

This commit is contained in:
Thomas Andres Gomez 2022-06-21 16:44:52 +02:00
parent 98f0e94766
commit 4f3fabc4d6
10 changed files with 106 additions and 107 deletions

View file

@ -83,13 +83,13 @@ dependencies {
// Android core
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0'
implementation 'com.google.android.material:material:1.6.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.1"
implementation 'androidx.activity:activity-compose:1.4.0'
// Android Compose
implementation "androidx.compose.ui:ui:1.2.0-alpha08"
implementation "androidx.compose.ui:ui:1.2.0-beta01"
implementation "androidx.compose.material:material:1.1.1"
implementation "androidx.compose.runtime:runtime-livedata:1.1.1"
implementation "androidx.compose.ui:ui-tooling-preview:1.1.1"

View file

@ -0,0 +1,21 @@
package com.pixelized.biblib.ui.composable
import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.pixelized.biblib.ui.screen.home.common.uio.CoverUio
@Composable
fun Cover(
modifier: Modifier = Modifier,
cover: CoverUio,
contentDescription: String? = null,
) {
Image(
modifier = modifier,
painter = cover.painter,
contentScale = cover.contentScale,
colorFilter = cover.colorFilter,
contentDescription = contentDescription,
)
}

View file

@ -46,7 +46,11 @@ fun ScreenNavHost(
fun NavHostController.navigateToHome() {
navigate(Screen.Home.route) {
launchSingleTop = true
popUpTo(0) { inclusive = true }
restoreState = true
popUpTo(0) {
saveState = true
inclusive = true
}
}
}

View file

@ -1,13 +1,21 @@
package com.pixelized.biblib.ui.scaffold
import androidx.compose.animation.ExperimentalAnimationApi
import android.content.Context
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material.*
import androidx.compose.material.BottomSheetScaffold
import androidx.compose.material.BottomSheetScaffoldState
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.rememberBottomSheetScaffoldState
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.biblib.R
import com.pixelized.biblib.ui.composable.StateUio
import com.pixelized.biblib.ui.screen.detail.BookDetailViewModel
import com.pixelized.biblib.ui.screen.detail.DetailScreen
import com.pixelized.biblib.ui.screen.home.common.uio.BookUio
import com.pixelized.biblib.utils.extention.showToast
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@ -15,7 +23,7 @@ val LocalBottomDetailController = staticCompositionLocalOf<BottomDetailStateCont
error("LocalBottomDetailController is not ready yet")
}
@OptIn(ExperimentalMaterialApi::class, ExperimentalAnimationApi::class)
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun BottomDetailScaffold(
bottomStateController: BottomDetailStateController = rememberBottomDetailStateController(),
@ -29,16 +37,8 @@ fun BottomDetailScaffold(
sheetPeekHeight = 0.dp,
sheetGesturesEnabled = false,
sheetContent = {
var detail by remember { bottomStateController.detail }
val state = bottomStateController.scaffoldState.bottomSheetState
if (state.currentValue == BottomSheetValue.Collapsed) {
detail = null
}
detail?.let {
val viewModel: BookDetailViewModel = hiltViewModel()
viewModel.getDetail(id = it)
DetailScreen(viewModel)
}
val detail by remember { bottomStateController.bookDetail }
DetailScreen(detail = detail)
},
content = content
)
@ -48,35 +48,49 @@ fun BottomDetailScaffold(
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun rememberBottomDetailStateController(
viewModel: BookDetailViewModel = hiltViewModel(),
scope: CoroutineScope = rememberCoroutineScope(),
scaffoldState: BottomSheetScaffoldState = rememberBottomSheetScaffoldState(),
): BottomDetailStateController {
val context: Context = LocalContext.current
val controller = BottomDetailStateController(
context = context,
viewModel = viewModel,
scope = scope,
scaffoldState = scaffoldState
)
return remember(scope, scaffoldState) { controller }
return remember(scope, viewModel, scaffoldState) { controller }
}
@OptIn(ExperimentalMaterialApi::class)
@Stable
class BottomDetailStateController constructor(
private val context: Context,
private val viewModel: BookDetailViewModel,
private val scope: CoroutineScope,
val scaffoldState: BottomSheetScaffoldState,
) {
val detail = mutableStateOf<Int?>(null)
var bookDetail = mutableStateOf<BookUio?>(null)
private set
fun expandBookDetail(bookId: Int) {
fun expandBookDetail(id: Int) {
scope.launch {
detail.value = bookId
scaffoldState.bottomSheetState.expand()
when (val book = viewModel.getDetail(id)) {
is StateUio.Failure -> {
context.showToast(message = context.getString(R.string.error_generic))
}
is StateUio.Success -> {
bookDetail.value = book.value
scaffoldState.bottomSheetState.expand()
}
else -> Unit
}
}
}
fun collapse() {
scope.launch {
scaffoldState.bottomSheetState.collapse()
detail.value = null
}
}
}

View file

@ -2,19 +2,13 @@ package com.pixelized.biblib.ui.screen.detail
import android.app.Application
import android.util.Log
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.layout.ContentScale
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.pixelized.biblib.R
import com.pixelized.biblib.model.book.Book
import com.pixelized.biblib.network.client.IBibLibClient
import com.pixelized.biblib.network.factory.BookFactory
import com.pixelized.biblib.ui.composable.StateUio
import com.pixelized.biblib.ui.navigation.screen.Screen
import com.pixelized.biblib.ui.screen.home.common.uio.BookUio
import com.pixelized.biblib.ui.screen.home.common.uio.CoverUio
import com.pixelized.biblib.ui.screen.home.common.viewModel.ACoverViewModel
@ -25,7 +19,7 @@ import com.pixelized.biblib.utils.extention.shortDate
import com.pixelized.biblib.utils.painterResource
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.URL
import javax.inject.Inject
@ -33,25 +27,17 @@ import javax.inject.Inject
class BookDetailViewModel @Inject constructor(
application: Application,
bookCoverCache: BookCoverCache,
savedStateHandle: SavedStateHandle,
private val client: IBibLibClient,
) : ACoverViewModel(application, bookCoverCache) {
private val _state = mutableStateOf<StateUio<BookUio>>(StateUio.Progress())
val state: State<StateUio<BookUio>> get() = _state
val book: State<BookUio?> = derivedStateOf {
state.value.let { if (it is StateUio.Success<BookUio>) it.value else null }
}
fun getDetail(id: Int) {
viewModelScope.launch(Dispatchers.IO) {
suspend fun getDetail(id: Int): StateUio<BookUio> {
return withContext(Dispatchers.IO) {
try {
val book = getBookDetail(id = id)
_state.value = StateUio.Success(book)
StateUio.Success(book)
} catch (exception: Exception) {
Log.e("BookDetailViewModel", exception.message, exception)
_state.value = StateUio.Failure(exception)
StateUio.Failure(exception)
}
}
}
@ -83,7 +69,7 @@ class BookDetailViewModel @Inject constructor(
date = releaseDate.shortDate(),
series = series?.name,
description = synopsis ?: "",
cover = cover(
coverState = cover(
placeHolder = thumbnailCover,
type = CoverUio.Type.DETAIL,
contentScale = ContentScale.FillHeight,
@ -91,7 +77,4 @@ class BookDetailViewModel @Inject constructor(
)
)
}
private val SavedStateHandle.bookId: Int
get() = get<Int>(Screen.BookDetail.ARG_BOOK_ID) ?: error("")
}

View file

@ -3,7 +3,6 @@ package com.pixelized.biblib.ui.screen.detail
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.activity.compose.BackHandler
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
@ -16,7 +15,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.Send
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@ -30,8 +28,8 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.text.HtmlCompat
import androidx.core.text.toSpannable
import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.biblib.R
import com.pixelized.biblib.ui.composable.Cover
import com.pixelized.biblib.ui.composable.SpannedText
import com.pixelized.biblib.ui.composable.animation.AnimatedDelayer
import com.pixelized.biblib.ui.composable.animation.AnimatedOffset
@ -45,29 +43,27 @@ import com.pixelized.biblib.utils.extention.todo
@Composable
fun DetailScreen(
viewModel: BookDetailViewModel = hiltViewModel()
detail: BookUio? = null,
) {
val bottomDetailState = LocalBottomDetailController.current
if (detail != null) {
DetailScreenContent(
modifier = Modifier.fillMaxSize(),
book = detail,
)
} else {
Box(modifier = Modifier.fillMaxSize())
}
Box {
val book by viewModel.book
book?.let {
DetailScreenContent(
book = it,
onClose = {
bottomDetailState.collapse()
},
)
}
val bottomDetailState = LocalBottomDetailController.current
BackHandler {
bottomDetailState.collapse()
}
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun DetailScreenContent(
modifier: Modifier = Modifier,
book: BookUio,
onClose: () -> Unit = todo(),
onMobi: () -> Unit = todo(),
onEpub: () -> Unit = todo(),
onSend: () -> Unit = todo(),
@ -87,36 +83,15 @@ private fun DetailScreenContent(
.height(MaterialTheme.bibLib.dimen.detail.cover),
contentAlignment = Alignment.Center
) {
val cover by book.cover
when (cover.type) {
CoverUio.Type.PLACE_HOLDER -> {
Image(
modifier = Modifier.size(MaterialTheme.bibLib.dimen.detail.placeHolder),
painter = cover.painter,
contentScale = cover.contentScale,
colorFilter = cover.colorFilter,
contentDescription = null,
)
}
CoverUio.Type.THUMBNAIL -> {
Image(
modifier = Modifier.fillMaxSize(),
painter = cover.painter,
contentScale = cover.contentScale,
colorFilter = cover.colorFilter,
contentDescription = null,
)
}
CoverUio.Type.DETAIL -> {
Image(
modifier = Modifier.fillMaxSize(),
painter = cover.painter,
contentScale = cover.contentScale,
colorFilter = cover.colorFilter,
contentDescription = null,
)
}
val sizeModifier = if (book.cover.type == CoverUio.Type.PLACE_HOLDER) {
Modifier.size(MaterialTheme.bibLib.dimen.detail.placeHolder)
} else {
Modifier.fillMaxSize()
}
Cover(
modifier = sizeModifier,
cover = book.cover,
)
}
Row(modifier = Modifier.padding(vertical = MaterialTheme.bibLib.dimen.medium)) {
@ -195,7 +170,7 @@ private fun DetailScreenContent(
AnimatedOffset(modifier = Modifier.weight(1f)) {
TitleLabel(
title = stringResource(id = R.string.detail_rating),
label = book.rating.toString(),
label = book.rating?.toString(),
)
}
@ -206,13 +181,11 @@ private fun DetailScreenContent(
)
}
book.date?.let {
AnimatedOffset(modifier = Modifier.weight(1f)) {
TitleLabel(
title = stringResource(id = R.string.detail_release),
label = it,
)
}
AnimatedOffset(modifier = Modifier.weight(1f)) {
TitleLabel(
title = stringResource(id = R.string.detail_release),
label = book.date ?: "-" ,
)
}
}
@ -237,8 +210,6 @@ private fun DetailScreenContent(
}
}
}
BackHandler(onBack = onClose)
}
@Composable
@ -288,7 +259,7 @@ private fun DetailScreenContentPreview() {
description = "En ce début de treizième millénaire, l'Empire n'a jamais été aussi puissant, aussi étendu à travers toute la galaxie. C'est dans sa capitale, Trantor, que l'éminent savant Hari Seldon invente la psychohistoire, une science nouvelle permettant de prédire l'avenir. Grâce à elle, Seldon prévoit l'effondrement de l'Empire d'ici cinq siècles, suivi d'une ère de ténèbres de trente mille ans. Réduire cette période à mille ans est peut-être possible, à condition de mener à terme son projet : la Fondation, chargée de rassembler toutes les connaissances humaines. Une entreprise visionnaire qui rencontre de nombreux et puissants détracteurs...",
rating = 4.5f,
language = "Français",
cover = cover,
coverState = cover,
)
BibLibTheme {
DetailScreenContent(book = book)

View file

@ -39,7 +39,10 @@ fun LazyBookThumbnailColumn(
contentPadding = contentPadding,
state = state,
) {
items(books) { thumbnail ->
items(
items = books,
key = { it.id },
) { thumbnail ->
BookThumbnail(
thumbnail = thumbnail,
onClick = currentOnItemClick,

View file

@ -1,6 +1,7 @@
package com.pixelized.biblib.ui.screen.home.common.uio
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
data class BookUio(
val id: Int,
@ -11,5 +12,7 @@ data class BookUio(
val date: String?,
val series: String?,
val description: String,
val cover: State<CoverUio>,
)
private val coverState: State<CoverUio>,
) {
val cover by coverState
}

View file

@ -24,7 +24,7 @@ fun BooksPage(
contentPadding = PaddingValues(all = MaterialTheme.bibLib.dimen.thumbnail.padding),
books = booksViewModel.books,
onItemClick = {
bottomDetailState.expandBookDetail(bookId = it.id)
bottomDetailState.expandBookDetail(id = it.id)
},
)
}

View file

@ -23,7 +23,7 @@ fun NewsPage(
contentPadding = PaddingValues(all = MaterialTheme.bibLib.dimen.thumbnail.padding),
books = booksViewModel.news,
onItemClick = {
bottomDetail.expandBookDetail(bookId = it.id)
bottomDetail.expandBookDetail(id = it.id)
},
)
}