Book detail error management like a boss :)

This commit is contained in:
Thomas Andres Gomez 2022-10-21 11:49:08 +02:00
parent 2774f93b6c
commit f48dfd6488
6 changed files with 160 additions and 69 deletions

View file

@ -7,17 +7,11 @@ import androidx.activity.viewModels
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.graphics.Color
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import com.pixelized.biblib.ui.composable.SystemThemeColor
import com.pixelized.biblib.ui.navigation.ScreenNavHost
import com.pixelized.biblib.ui.screen.launch.LauncherViewModel
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.skydoves.landscapist.glide.LocalGlideRequestOptions
import dagger.hilt.android.AndroidEntryPoint
@ -42,14 +36,9 @@ class MainActivity : ComponentActivity() {
// Compose
setContent {
BibLibActivityTheme {
ProvideGlideOption {
// Handle the main Navigation
if (launcherViewModel.isLoadingDone) {
ScreenNavHost(
startDestination = launcherViewModel.startDestination
)
}
}
MainContent(
launcherViewModel = launcherViewModel
)
}
}
}
@ -67,16 +56,4 @@ class MainActivity : ComponentActivity() {
}
}
}
@Composable
private fun ProvideGlideOption(
options: RequestOptions = RequestOptions().diskCacheStrategy(DiskCacheStrategy.ALL),
content: @Composable () -> Unit,
) {
CompositionLocalProvider(
LocalGlideRequestOptions provides options,
) {
content()
}
}
}

View file

@ -0,0 +1,56 @@
package com.pixelized.biblib.ui
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import com.pixelized.biblib.ui.navigation.ScreenNavHost
import com.pixelized.biblib.ui.screen.launch.LauncherViewModel
import com.pixelized.biblib.utils.extention.bibLib
import com.skydoves.landscapist.glide.LocalGlideRequestOptions
val LocalSnackHostState = staticCompositionLocalOf<SnackbarHostState> {
error("SnackBarHostState is not ready yet.")
}
@Composable
fun MainContent(
launcherViewModel: LauncherViewModel = hiltViewModel(),
glideOptions: RequestOptions = RequestOptions().diskCacheStrategy(DiskCacheStrategy.ALL)
) {
val scaffoldState: ScaffoldState = rememberScaffoldState()
CompositionLocalProvider(
LocalGlideRequestOptions provides glideOptions,
LocalSnackHostState provides scaffoldState.snackbarHostState,
) {
Scaffold(
scaffoldState = scaffoldState,
snackbarHost = { snackHostState ->
SnackbarHost(
modifier = Modifier.systemBarsPadding(),
hostState = snackHostState,
) { snackBarData ->
Snackbar(
snackbarData = snackBarData,
backgroundColor = MaterialTheme.colors.error,
contentColor = MaterialTheme.colors.onError,
actionColor = MaterialTheme.colors.onError,
)
}
},
content = {
if (launcherViewModel.isLoadingDone) {
ScreenNavHost(
startDestination = launcherViewModel.startDestination
)
}
}
)
}
}

View file

@ -1,18 +1,19 @@
package com.pixelized.biblib.ui.screen.home.detail
import android.app.Application
import android.util.Log
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.biblib.R
import com.pixelized.biblib.network.client.IBibLibClient
import com.pixelized.biblib.network.factory.BookFactory
import com.pixelized.biblib.repository.book.BookRepository
import com.pixelized.biblib.ui.screen.home.detail.BookDetailUioErrorUio.Type
import com.pixelized.biblib.utils.extention.stringResource
import com.pixelized.biblib.utils.extention.toDetailUio
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
@ -20,37 +21,41 @@ import javax.inject.Inject
@HiltViewModel
class BookDetailViewModel @Inject constructor(
application: Application,
private val bookRepository: BookRepository,
private val client: IBibLibClient,
) : ViewModel() {
) : AndroidViewModel(application) {
private val _detail = mutableStateOf<BookDetailUio?>(null)
val detail: State<BookDetailUio?> get() = _detail
private val _sendStatus = MutableSharedFlow<Boolean>()
val sendStatus: Flow<Boolean> get() = _sendStatus
private val _error = MutableSharedFlow<BookDetailUioErrorUio>()
val error: Flow<BookDetailUioErrorUio> get() = _error
fun getDetail(id: Int?): State<BookDetailUio?> {
return mutableStateOf<BookDetailUio?>(null).apply {
viewModelScope.launch(Dispatchers.IO) {
try {
requireNotNull(id)
value = getCacheBookDetail(id = id)
value = getBookDetail(id = id)
} catch (exception: Exception) {
_error.emit(exception.toUio(Type.GET_DETAIL))
}
fun getDetail(id: Int) {
viewModelScope.launch(Dispatchers.IO) {
try {
_detail.value = getCacheBookDetail(id = id)
_detail.value = getBookDetail(id = id)
} catch (exception: Exception) {
_error.emit(toDetailErrorUio(bookId = id))
}
}
}
suspend fun send(bookId: Int, email: String): State<Boolean?> {
return mutableStateOf<Boolean?>(null).apply {
viewModelScope.launch(Dispatchers.IO) {
try {
val data = client.service.send(bookId = bookId, mail = email)
Log.d("send", data.toString())
} catch (exception: Exception) {
Log.d("send", exception.message, exception)
_error.emit(exception.toUio(Type.SEND_BOOK))
}
suspend fun send(bookId: Int, email: String) {
viewModelScope.launch(Dispatchers.IO) {
try {
val data = client.service.send(bookId = bookId, mail = email)
_sendStatus.emit(true)
Log.d("send", data.toString())
} catch (exception: Exception) {
Log.d("send", exception.message, exception)
_sendStatus.emit(false)
_error.emit(toSendBookUio(bookId = bookId, mail = email))
}
}
}
@ -66,8 +71,16 @@ class BookDetailViewModel @Inject constructor(
return book.toDetailUio()
}
private fun Exception.toUio(type: Type) = BookDetailUioErrorUio(
type = type,
message = this.message ?: "An error occurred."
private fun toDetailErrorUio(bookId: Int) = BookDetailUioErrorUio.GetDetailInput(
message = stringResource(R.string.error_get_book_detail_message),
action = stringResource(R.string.error_get_book_detail_action),
bookId = bookId,
)
private fun toSendBookUio(bookId: Int, mail: String) = BookDetailUioErrorUio.SendBookInput(
message = stringResource(R.string.error_send_book_message),
action = stringResource(R.string.error_send_book_action),
bookId = bookId,
mail = mail,
)
}

View file

@ -6,12 +6,12 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.biblib.R
import com.pixelized.biblib.ui.LocalSnackHostState
import com.pixelized.biblib.ui.composable.isSuccessful
import com.pixelized.biblib.ui.scaffold.LocalDetailBottomSheetState
import com.pixelized.biblib.ui.screen.home.page.profile.ProfileViewModel
@ -51,14 +51,26 @@ data class BookDetailUio(
@Stable
@Immutable
data class BookDetailUioErrorUio(
val type: Type,
sealed class BookDetailUioErrorUio(
val message: String,
val action: String?,
) {
enum class Type {
GET_DETAIL,
SEND_BOOK,
}
@Stable
@Immutable
class GetDetailInput(
message: String,
action: String?,
val bookId: Int,
) : BookDetailUioErrorUio(message, action)
@Stable
@Immutable
class SendBookInput(
message: String,
action: String?,
val bookId: Int,
val mail: String,
) : BookDetailUioErrorUio(message, action)
}
@OptIn(ExperimentalMaterialApi::class)
@ -70,6 +82,7 @@ fun DetailScreen(
bookId: Int? = null,
) {
val detailState = LocalDetailBottomSheetState.current
val snackBarHost = LocalSnackHostState.current
val uriHandler = LocalUriHandler.current
val context = LocalContext.current
@ -79,11 +92,7 @@ fun DetailScreen(
skipHalfExpanded = true,
)
val bookDetailState: State<BookDetailUio?> = rememberSaveable(bookId) {
detailViewModel.getDetail(bookId)
}
when (val detail = bookDetailState.value) {
when (val detail = detailViewModel.detail.value) {
null -> EmptyDetail()
else -> {
ModalBottomSheetLayout(
@ -155,16 +164,36 @@ fun DetailScreen(
}
}
LaunchedEffect(key1 = "DetailScreenError") {
detailViewModel.error.collect {
context.showToast(message = it.message)
if (detailState.bottomSheetState.isVisible || detailState.bottomSheetState.isAnimationRunning) {
LaunchedEffect(key1 = "DetailScreenError") {
detailViewModel.error.collect {
val result = snackBarHost.showSnackbar(
message = it.message,
actionLabel = it.action,
duration = SnackbarDuration.Indefinite,
)
if (result == SnackbarResult.ActionPerformed) {
when (it) {
is BookDetailUioErrorUio.GetDetailInput -> {
detailViewModel.getDetail(id = it.bookId)
}
is BookDetailUioErrorUio.SendBookInput -> {
detailViewModel.send(bookId = it.bookId, email = it.mail)
}
}
}
}
}
LaunchedEffect(key1 = bookId) {
bookId?.let { detailViewModel.getDetail(it) }
}
}
}
@Composable
private fun EmptyDetail(
modifier : Modifier = Modifier,
modifier: Modifier = Modifier,
) = Box(
modifier = modifier.fillMaxSize()
)

View file

@ -14,6 +14,14 @@
<string name="menu_author">Auteurs</string>
<string name="menu_tag">Genres</string>
<!-- Error -->
<string name="error_action_retry">Réessayer</string>
<string name="error_get_book_detail_message">La récupération des détails a échouée.</string>
<string name="error_get_book_detail_action">@string/error_action_retry</string>
<string name="error_send_book_message">L\'envoi de l\'eBook a échoué.</string>
<string name="error_send_book_action">@string/error_action_retry</string>
<!-- Dialogs -->
<string name="error_generic">Oups!</string>

View file

@ -20,6 +20,14 @@
<string name="menu_author">Authors</string>
<string name="menu_tag">Genres</string>
<!-- Error -->
<string name="error_action_retry">Retry</string>
<string name="error_get_book_detail_message">Failed to retrieve book details.</string>
<string name="error_get_book_detail_action">@string/error_action_retry</string>
<string name="error_send_book_message">Failed to send the book to your kindle.</string>
<string name="error_send_book_action">@string/error_action_retry</string>
<!-- Dialogs -->
<string name="error_generic">Oops!</string>