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 // Android core
implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1' 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-runtime-ktx:2.4.1'
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.1" implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.1"
implementation 'androidx.activity:activity-compose:1.4.0' implementation 'androidx.activity:activity-compose:1.4.0'
// Android Compose // 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.material:material:1.1.1"
implementation "androidx.compose.runtime:runtime-livedata:1.1.1" implementation "androidx.compose.runtime:runtime-livedata:1.1.1"
implementation "androidx.compose.ui:ui-tooling-preview: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() { fun NavHostController.navigateToHome() {
navigate(Screen.Home.route) { navigate(Screen.Home.route) {
launchSingleTop = true 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 package com.pixelized.biblib.ui.scaffold
import androidx.compose.animation.ExperimentalAnimationApi import android.content.Context
import androidx.compose.foundation.layout.PaddingValues 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.runtime.*
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel 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.BookDetailViewModel
import com.pixelized.biblib.ui.screen.detail.DetailScreen 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.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -15,7 +23,7 @@ val LocalBottomDetailController = staticCompositionLocalOf<BottomDetailStateCont
error("LocalBottomDetailController is not ready yet") error("LocalBottomDetailController is not ready yet")
} }
@OptIn(ExperimentalMaterialApi::class, ExperimentalAnimationApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
fun BottomDetailScaffold( fun BottomDetailScaffold(
bottomStateController: BottomDetailStateController = rememberBottomDetailStateController(), bottomStateController: BottomDetailStateController = rememberBottomDetailStateController(),
@ -29,16 +37,8 @@ fun BottomDetailScaffold(
sheetPeekHeight = 0.dp, sheetPeekHeight = 0.dp,
sheetGesturesEnabled = false, sheetGesturesEnabled = false,
sheetContent = { sheetContent = {
var detail by remember { bottomStateController.detail } val detail by remember { bottomStateController.bookDetail }
val state = bottomStateController.scaffoldState.bottomSheetState DetailScreen(detail = detail)
if (state.currentValue == BottomSheetValue.Collapsed) {
detail = null
}
detail?.let {
val viewModel: BookDetailViewModel = hiltViewModel()
viewModel.getDetail(id = it)
DetailScreen(viewModel)
}
}, },
content = content content = content
) )
@ -48,35 +48,49 @@ fun BottomDetailScaffold(
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
fun rememberBottomDetailStateController( fun rememberBottomDetailStateController(
viewModel: BookDetailViewModel = hiltViewModel(),
scope: CoroutineScope = rememberCoroutineScope(), scope: CoroutineScope = rememberCoroutineScope(),
scaffoldState: BottomSheetScaffoldState = rememberBottomSheetScaffoldState(), scaffoldState: BottomSheetScaffoldState = rememberBottomSheetScaffoldState(),
): BottomDetailStateController { ): BottomDetailStateController {
val context: Context = LocalContext.current
val controller = BottomDetailStateController( val controller = BottomDetailStateController(
context = context,
viewModel = viewModel,
scope = scope, scope = scope,
scaffoldState = scaffoldState scaffoldState = scaffoldState
) )
return remember(scope, scaffoldState) { controller } return remember(scope, viewModel, scaffoldState) { controller }
} }
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Stable @Stable
class BottomDetailStateController constructor( class BottomDetailStateController constructor(
private val context: Context,
private val viewModel: BookDetailViewModel,
private val scope: CoroutineScope, private val scope: CoroutineScope,
val scaffoldState: BottomSheetScaffoldState, 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 { scope.launch {
detail.value = bookId when (val book = viewModel.getDetail(id)) {
scaffoldState.bottomSheetState.expand() 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() { fun collapse() {
scope.launch { scope.launch {
scaffoldState.bottomSheetState.collapse() 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.app.Application
import android.util.Log import android.util.Log
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.pixelized.biblib.R import com.pixelized.biblib.R
import com.pixelized.biblib.model.book.Book import com.pixelized.biblib.model.book.Book
import com.pixelized.biblib.network.client.IBibLibClient import com.pixelized.biblib.network.client.IBibLibClient
import com.pixelized.biblib.network.factory.BookFactory import com.pixelized.biblib.network.factory.BookFactory
import com.pixelized.biblib.ui.composable.StateUio 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.BookUio
import com.pixelized.biblib.ui.screen.home.common.uio.CoverUio import com.pixelized.biblib.ui.screen.home.common.uio.CoverUio
import com.pixelized.biblib.ui.screen.home.common.viewModel.ACoverViewModel 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 com.pixelized.biblib.utils.painterResource
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.withContext
import java.net.URL import java.net.URL
import javax.inject.Inject import javax.inject.Inject
@ -33,25 +27,17 @@ import javax.inject.Inject
class BookDetailViewModel @Inject constructor( class BookDetailViewModel @Inject constructor(
application: Application, application: Application,
bookCoverCache: BookCoverCache, bookCoverCache: BookCoverCache,
savedStateHandle: SavedStateHandle,
private val client: IBibLibClient, private val client: IBibLibClient,
) : ACoverViewModel(application, bookCoverCache) { ) : ACoverViewModel(application, bookCoverCache) {
private val _state = mutableStateOf<StateUio<BookUio>>(StateUio.Progress()) suspend fun getDetail(id: Int): StateUio<BookUio> {
val state: State<StateUio<BookUio>> get() = _state return withContext(Dispatchers.IO) {
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) {
try { try {
val book = getBookDetail(id = id) val book = getBookDetail(id = id)
_state.value = StateUio.Success(book) StateUio.Success(book)
} catch (exception: Exception) { } catch (exception: Exception) {
Log.e("BookDetailViewModel", exception.message, 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(), date = releaseDate.shortDate(),
series = series?.name, series = series?.name,
description = synopsis ?: "", description = synopsis ?: "",
cover = cover( coverState = cover(
placeHolder = thumbnailCover, placeHolder = thumbnailCover,
type = CoverUio.Type.DETAIL, type = CoverUio.Type.DETAIL,
contentScale = ContentScale.FillHeight, 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_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState 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.Download
import androidx.compose.material.icons.filled.Send import androidx.compose.material.icons.filled.Send
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -30,8 +28,8 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
import androidx.core.text.toSpannable import androidx.core.text.toSpannable
import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.biblib.R 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.SpannedText
import com.pixelized.biblib.ui.composable.animation.AnimatedDelayer import com.pixelized.biblib.ui.composable.animation.AnimatedDelayer
import com.pixelized.biblib.ui.composable.animation.AnimatedOffset import com.pixelized.biblib.ui.composable.animation.AnimatedOffset
@ -45,29 +43,27 @@ import com.pixelized.biblib.utils.extention.todo
@Composable @Composable
fun DetailScreen( 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 bottomDetailState = LocalBottomDetailController.current
val book by viewModel.book BackHandler {
book?.let { bottomDetailState.collapse()
DetailScreenContent(
book = it,
onClose = {
bottomDetailState.collapse()
},
)
}
} }
} }
@OptIn(ExperimentalAnimationApi::class)
@Composable @Composable
private fun DetailScreenContent( private fun DetailScreenContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
book: BookUio, book: BookUio,
onClose: () -> Unit = todo(),
onMobi: () -> Unit = todo(), onMobi: () -> Unit = todo(),
onEpub: () -> Unit = todo(), onEpub: () -> Unit = todo(),
onSend: () -> Unit = todo(), onSend: () -> Unit = todo(),
@ -87,36 +83,15 @@ private fun DetailScreenContent(
.height(MaterialTheme.bibLib.dimen.detail.cover), .height(MaterialTheme.bibLib.dimen.detail.cover),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
val cover by book.cover val sizeModifier = if (book.cover.type == CoverUio.Type.PLACE_HOLDER) {
when (cover.type) { Modifier.size(MaterialTheme.bibLib.dimen.detail.placeHolder)
CoverUio.Type.PLACE_HOLDER -> { } else {
Image( Modifier.fillMaxSize()
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,
)
}
} }
Cover(
modifier = sizeModifier,
cover = book.cover,
)
} }
Row(modifier = Modifier.padding(vertical = MaterialTheme.bibLib.dimen.medium)) { Row(modifier = Modifier.padding(vertical = MaterialTheme.bibLib.dimen.medium)) {
@ -195,7 +170,7 @@ private fun DetailScreenContent(
AnimatedOffset(modifier = Modifier.weight(1f)) { AnimatedOffset(modifier = Modifier.weight(1f)) {
TitleLabel( TitleLabel(
title = stringResource(id = R.string.detail_rating), 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)) {
AnimatedOffset(modifier = Modifier.weight(1f)) { TitleLabel(
TitleLabel( title = stringResource(id = R.string.detail_release),
title = stringResource(id = R.string.detail_release), label = book.date ?: "-" ,
label = it, )
)
}
} }
} }
@ -237,8 +210,6 @@ private fun DetailScreenContent(
} }
} }
} }
BackHandler(onBack = onClose)
} }
@Composable @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...", 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, rating = 4.5f,
language = "Français", language = "Français",
cover = cover, coverState = cover,
) )
BibLibTheme { BibLibTheme {
DetailScreenContent(book = book) DetailScreenContent(book = book)

View file

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

View file

@ -1,6 +1,7 @@
package com.pixelized.biblib.ui.screen.home.common.uio package com.pixelized.biblib.ui.screen.home.common.uio
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
data class BookUio( data class BookUio(
val id: Int, val id: Int,
@ -11,5 +12,7 @@ data class BookUio(
val date: String?, val date: String?,
val series: String?, val series: String?,
val description: 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), contentPadding = PaddingValues(all = MaterialTheme.bibLib.dimen.thumbnail.padding),
books = booksViewModel.books, books = booksViewModel.books,
onItemClick = { 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), contentPadding = PaddingValues(all = MaterialTheme.bibLib.dimen.thumbnail.padding),
books = booksViewModel.news, books = booksViewModel.news,
onItemClick = { onItemClick = {
bottomDetail.expandBookDetail(bookId = it.id) bottomDetail.expandBookDetail(id = it.id)
}, },
) )
} }