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.MaterialTheme
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.runtime.Composable 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.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat 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.composable.SystemThemeColor
import com.pixelized.biblib.ui.navigation.ScreenNavHost
import com.pixelized.biblib.ui.screen.launch.LauncherViewModel import com.pixelized.biblib.ui.screen.launch.LauncherViewModel
import com.pixelized.biblib.ui.theme.BibLibTheme import com.pixelized.biblib.ui.theme.BibLibTheme
import com.skydoves.landscapist.glide.LocalGlideRequestOptions
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@ -42,14 +36,9 @@ class MainActivity : ComponentActivity() {
// Compose // Compose
setContent { setContent {
BibLibActivityTheme { BibLibActivityTheme {
ProvideGlideOption { MainContent(
// Handle the main Navigation launcherViewModel = launcherViewModel
if (launcherViewModel.isLoadingDone) { )
ScreenNavHost(
startDestination = launcherViewModel.startDestination
)
}
}
} }
} }
} }
@ -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 package com.pixelized.biblib.ui.screen.home.detail
import android.app.Application
import android.util.Log import android.util.Log
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.pixelized.biblib.R
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.repository.book.BookRepository 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 com.pixelized.biblib.utils.extention.toDetailUio
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -20,37 +21,41 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class BookDetailViewModel @Inject constructor( class BookDetailViewModel @Inject constructor(
application: Application,
private val bookRepository: BookRepository, private val bookRepository: BookRepository,
private val client: IBibLibClient, 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>() private val _error = MutableSharedFlow<BookDetailUioErrorUio>()
val error: Flow<BookDetailUioErrorUio> get() = _error val error: Flow<BookDetailUioErrorUio> get() = _error
fun getDetail(id: Int?): State<BookDetailUio?> { fun getDetail(id: Int) {
return mutableStateOf<BookDetailUio?>(null).apply { viewModelScope.launch(Dispatchers.IO) {
viewModelScope.launch(Dispatchers.IO) { try {
try { _detail.value = getCacheBookDetail(id = id)
requireNotNull(id) _detail.value = getBookDetail(id = id)
value = getCacheBookDetail(id = id) } catch (exception: Exception) {
value = getBookDetail(id = id) _error.emit(toDetailErrorUio(bookId = id))
} catch (exception: Exception) {
_error.emit(exception.toUio(Type.GET_DETAIL))
}
} }
} }
} }
suspend fun send(bookId: Int, email: String): State<Boolean?> { suspend fun send(bookId: Int, email: String) {
return mutableStateOf<Boolean?>(null).apply { viewModelScope.launch(Dispatchers.IO) {
viewModelScope.launch(Dispatchers.IO) { try {
try { val data = client.service.send(bookId = bookId, mail = email)
val data = client.service.send(bookId = bookId, mail = email) _sendStatus.emit(true)
Log.d("send", data.toString()) Log.d("send", data.toString())
} catch (exception: Exception) { } catch (exception: Exception) {
Log.d("send", exception.message, exception) Log.d("send", exception.message, exception)
_error.emit(exception.toUio(Type.SEND_BOOK)) _sendStatus.emit(false)
} _error.emit(toSendBookUio(bookId = bookId, mail = email))
} }
} }
} }
@ -66,8 +71,16 @@ class BookDetailViewModel @Inject constructor(
return book.toDetailUio() return book.toDetailUio()
} }
private fun Exception.toUio(type: Type) = BookDetailUioErrorUio( private fun toDetailErrorUio(bookId: Int) = BookDetailUioErrorUio.GetDetailInput(
type = type, message = stringResource(R.string.error_get_book_detail_message),
message = this.message ?: "An error occurred." 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.foundation.verticalScroll
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.biblib.R import com.pixelized.biblib.R
import com.pixelized.biblib.ui.LocalSnackHostState
import com.pixelized.biblib.ui.composable.isSuccessful import com.pixelized.biblib.ui.composable.isSuccessful
import com.pixelized.biblib.ui.scaffold.LocalDetailBottomSheetState import com.pixelized.biblib.ui.scaffold.LocalDetailBottomSheetState
import com.pixelized.biblib.ui.screen.home.page.profile.ProfileViewModel import com.pixelized.biblib.ui.screen.home.page.profile.ProfileViewModel
@ -51,14 +51,26 @@ data class BookDetailUio(
@Stable @Stable
@Immutable @Immutable
data class BookDetailUioErrorUio( sealed class BookDetailUioErrorUio(
val type: Type,
val message: String, val message: String,
val action: String?,
) { ) {
enum class Type { @Stable
GET_DETAIL, @Immutable
SEND_BOOK, 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) @OptIn(ExperimentalMaterialApi::class)
@ -70,6 +82,7 @@ fun DetailScreen(
bookId: Int? = null, bookId: Int? = null,
) { ) {
val detailState = LocalDetailBottomSheetState.current val detailState = LocalDetailBottomSheetState.current
val snackBarHost = LocalSnackHostState.current
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
val context = LocalContext.current val context = LocalContext.current
@ -79,11 +92,7 @@ fun DetailScreen(
skipHalfExpanded = true, skipHalfExpanded = true,
) )
val bookDetailState: State<BookDetailUio?> = rememberSaveable(bookId) { when (val detail = detailViewModel.detail.value) {
detailViewModel.getDetail(bookId)
}
when (val detail = bookDetailState.value) {
null -> EmptyDetail() null -> EmptyDetail()
else -> { else -> {
ModalBottomSheetLayout( ModalBottomSheetLayout(
@ -155,16 +164,36 @@ fun DetailScreen(
} }
} }
LaunchedEffect(key1 = "DetailScreenError") { if (detailState.bottomSheetState.isVisible || detailState.bottomSheetState.isAnimationRunning) {
detailViewModel.error.collect { LaunchedEffect(key1 = "DetailScreenError") {
context.showToast(message = it.message) 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 @Composable
private fun EmptyDetail( private fun EmptyDetail(
modifier : Modifier = Modifier, modifier: Modifier = Modifier,
) = Box( ) = Box(
modifier = modifier.fillMaxSize() modifier = modifier.fillMaxSize()
) )

View file

@ -14,6 +14,14 @@
<string name="menu_author">Auteurs</string> <string name="menu_author">Auteurs</string>
<string name="menu_tag">Genres</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 --> <!-- Dialogs -->
<string name="error_generic">Oups!</string> <string name="error_generic">Oups!</string>

View file

@ -20,6 +20,14 @@
<string name="menu_author">Authors</string> <string name="menu_author">Authors</string>
<string name="menu_tag">Genres</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 --> <!-- Dialogs -->
<string name="error_generic">Oops!</string> <string name="error_generic">Oops!</string>