Book detail error management like a boss :)
This commit is contained in:
parent
2774f93b6c
commit
f48dfd6488
6 changed files with 160 additions and 69 deletions
|
|
@ -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,17 +36,12 @@ class MainActivity : ComponentActivity() {
|
|||
// Compose
|
||||
setContent {
|
||||
BibLibActivityTheme {
|
||||
ProvideGlideOption {
|
||||
// Handle the main Navigation
|
||||
if (launcherViewModel.isLoadingDone) {
|
||||
ScreenNavHost(
|
||||
startDestination = launcherViewModel.startDestination
|
||||
MainContent(
|
||||
launcherViewModel = launcherViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BibLibActivityTheme(
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
56
app/src/main/java/com/pixelized/biblib/ui/MainContent.kt
Normal file
56
app/src/main/java/com/pixelized/biblib/ui/MainContent.kt
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
fun getDetail(id: Int) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
requireNotNull(id)
|
||||
value = getCacheBookDetail(id = id)
|
||||
value = getBookDetail(id = id)
|
||||
_detail.value = getCacheBookDetail(id = id)
|
||||
_detail.value = getBookDetail(id = id)
|
||||
} catch (exception: Exception) {
|
||||
_error.emit(exception.toUio(Type.GET_DETAIL))
|
||||
}
|
||||
_error.emit(toDetailErrorUio(bookId = id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun send(bookId: Int, email: String): State<Boolean?> {
|
||||
return mutableStateOf<Boolean?>(null).apply {
|
||||
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)
|
||||
_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()
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,9 +164,29 @@ fun DetailScreen(
|
|||
}
|
||||
}
|
||||
|
||||
if (detailState.bottomSheetState.isVisible || detailState.bottomSheetState.isAnimationRunning) {
|
||||
LaunchedEffect(key1 = "DetailScreenError") {
|
||||
detailViewModel.error.collect {
|
||||
context.showToast(message = it.message)
|
||||
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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue