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.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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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
|
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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -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()
|
||||||
)
|
)
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue