pref: 加载错误显示

This commit is contained in:
HuanCheng65 2023-03-12 00:04:03 +08:00
parent addbc80bc0
commit cf6c42ae1c
No known key found for this signature in database
GPG Key ID: E9031EF91A805148
4 changed files with 357 additions and 89 deletions

View File

@ -6,7 +6,6 @@ import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
@ -81,8 +80,10 @@ import com.huanchengfly.tieba.post.ui.widgets.compose.ActionItem
import com.huanchengfly.tieba.post.ui.widgets.compose.Avatar
import com.huanchengfly.tieba.post.ui.widgets.compose.Button
import com.huanchengfly.tieba.post.ui.widgets.compose.ConfirmDialog
import com.huanchengfly.tieba.post.ui.widgets.compose.ErrorScreen
import com.huanchengfly.tieba.post.ui.widgets.compose.LongClickMenu
import com.huanchengfly.tieba.post.ui.widgets.compose.TextButton
import com.huanchengfly.tieba.post.ui.widgets.compose.TipScreen
import com.huanchengfly.tieba.post.ui.widgets.compose.Toolbar
import com.huanchengfly.tieba.post.ui.widgets.compose.accountNavIconIfCompact
import com.huanchengfly.tieba.post.ui.widgets.compose.rememberDialogState
@ -153,8 +154,8 @@ fun SearchBox(
@Composable
private fun Header(
text: String,
invert: Boolean = false,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
invert: Boolean = false
) {
Chip(
text = text,
@ -391,6 +392,11 @@ fun HomePage(
prop1 = HomeUiState::topForums,
initial = emptyList()
)
val error by viewModel.uiState.collectPartialAsState(
prop1 = HomeUiState::error,
initial = null
)
val isError by remember { derivedStateOf { error != null } }
var listSingle by remember { mutableStateOf(context.appPreferences.listSingle) }
val gridCells by remember { derivedStateOf { getGridCells(context, listSingle) } }
@ -421,9 +427,12 @@ fun HomePage(
) { contentPaddings ->
StateScreen(
isEmpty = forums.isEmpty(),
isError = false,
isError = isError,
isLoading = isLoading,
modifier = Modifier.padding(contentPaddings),
onReload = {
viewModel.send(HomeUiIntent.Refresh)
},
emptyScreen = {
EmptyScreen(
loggedIn = account != null,
@ -433,6 +442,9 @@ fun HomePage(
},
loadingScreen = {
HomePageSkeletonScreen(listSingle = listSingle, gridCells = gridCells)
},
errorScreen = {
error?.let { ErrorScreen(error = it) }
}
) {
val pullRefreshState = rememberPullRefreshState(
@ -516,11 +528,11 @@ private fun HomePageSkeletonScreen(
Column {
Header(
text = stringResource(id = R.string.title_top_forum),
invert = true,
modifier = Modifier.placeholder(
visible = true,
color = ExtendedTheme.colors.chip
)
),
invert = true
)
Spacer(modifier = Modifier.height(8.dp))
}
@ -541,11 +553,11 @@ private fun HomePageSkeletonScreen(
Column {
Header(
text = stringResource(id = R.string.forum_list_title),
invert = true,
modifier = Modifier.placeholder(
visible = true,
color = ExtendedTheme.colors.chip
)
),
invert = true
)
Spacer(modifier = Modifier.height(8.dp))
}
@ -563,13 +575,15 @@ fun EmptyScreen(
onOpenExplore: () -> Unit
) {
val context = LocalContext.current
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp, alignment = CenterVertically)
) {
TipScreen(
title = {
if (!loggedIn) {
Text(text = stringResource(id = R.string.title_empty_login))
} else {
Text(text = stringResource(id = R.string.title_empty))
}
},
image = {
val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.lottie_astronaut))
LottieAnimation(
composition = composition,
@ -578,19 +592,19 @@ fun EmptyScreen(
.fillMaxWidth()
.aspectRatio(2f)
)
},
message = {
if (!loggedIn) {
Text(
text = stringResource(id = R.string.title_empty_login),
style = MaterialTheme.typography.h6,
color = ExtendedTheme.colors.text,
textAlign = TextAlign.Center,
)
Text(
text = stringResource(id = R.string.home_empty_login),
style = MaterialTheme.typography.body1,
color = ExtendedTheme.colors.textSecondary,
textAlign = TextAlign.Center
)
}
},
actions = {
if (!loggedIn) {
Button(
onClick = {
context.goToActivity<LoginActivity>()
@ -600,13 +614,6 @@ fun EmptyScreen(
) {
Text(text = stringResource(id = R.string.button_login))
}
} else {
Text(
text = stringResource(id = R.string.title_empty),
style = MaterialTheme.typography.h6,
color = ExtendedTheme.colors.text,
textAlign = TextAlign.Center,
)
}
if (canOpenExplore) {
TextButton(
@ -617,5 +624,6 @@ fun EmptyScreen(
Text(text = stringResource(id = R.string.button_go_to_explore))
}
}
}
},
)
}

View File

@ -15,24 +15,29 @@ import org.litepal.LitePal
class HomeViewModel : BaseViewModel<HomeUiIntent, HomePartialChange, HomeUiState, HomeUiEvent>() {
override fun createInitialState(): HomeUiState = HomeUiState()
override fun createPartialChangeProducer(): PartialChangeProducer<HomeUiIntent, HomePartialChange, HomeUiState> = HomePartialChangeProducer
override fun createPartialChangeProducer(): PartialChangeProducer<HomeUiIntent, HomePartialChange, HomeUiState> =
HomePartialChangeProducer
override fun dispatchEvent(partialChange: HomePartialChange): UiEvent? =
when (partialChange) {
is HomePartialChange.Refresh.Failure -> CommonUiEvent.Toast(partialChange.errorMessage)
is HomePartialChange.TopForums.Delete.Failure -> CommonUiEvent.Toast(partialChange.errorMessage)
is HomePartialChange.TopForums.Add.Failure -> CommonUiEvent.Toast(partialChange.errorMessage)
else -> null
}
object HomePartialChangeProducer : PartialChangeProducer<HomeUiIntent, HomePartialChange, HomeUiState> {
object HomePartialChangeProducer :
PartialChangeProducer<HomeUiIntent, HomePartialChange, HomeUiState> {
@OptIn(FlowPreview::class)
override fun toPartialChangeFlow(intentFlow: Flow<HomeUiIntent>): Flow<HomePartialChange> {
return merge(
intentFlow.filterIsInstance<HomeUiIntent.Refresh>().flatMapConcat { produceRefreshPartialChangeFlow() },
intentFlow.filterIsInstance<HomeUiIntent.TopForums.Delete>().flatMapConcat { it.toPartialChangeFlow() },
intentFlow.filterIsInstance<HomeUiIntent.TopForums.Add>().flatMapConcat { it.toPartialChangeFlow() },
intentFlow.filterIsInstance<HomeUiIntent.Unfollow>().flatMapConcat { it.toPartialChangeFlow() },
intentFlow.filterIsInstance<HomeUiIntent.Refresh>()
.flatMapConcat { produceRefreshPartialChangeFlow() },
intentFlow.filterIsInstance<HomeUiIntent.TopForums.Delete>()
.flatMapConcat { it.toPartialChangeFlow() },
intentFlow.filterIsInstance<HomeUiIntent.TopForums.Add>()
.flatMapConcat { it.toPartialChangeFlow() },
intentFlow.filterIsInstance<HomeUiIntent.Unfollow>()
.flatMapConcat { it.toPartialChangeFlow() },
)
}
@ -54,7 +59,7 @@ class HomeViewModel : BaseViewModel<HomeUiIntent, HomePartialChange, HomeUiState
HomePartialChange.Refresh.Success(forums, topForums)
}
.onStart { emit(HomePartialChange.Refresh.Start) }
.catch { emit(HomePartialChange.Refresh.Failure(it.getErrorMessage())) }
.catch { emit(HomePartialChange.Refresh.Failure(it)) }
private fun HomeUiIntent.TopForums.Delete.toPartialChangeFlow() =
flow {
@ -79,7 +84,8 @@ class HomeViewModel : BaseViewModel<HomeUiIntent, HomePartialChange, HomeUiState
.catch { emit(HomePartialChange.TopForums.Add.Failure(it.getErrorMessage())) }
private fun HomeUiIntent.Unfollow.toPartialChangeFlow() =
TiebaApi.getInstance().unlikeForumFlow(forumId, forumName, AccountUtil.getLoginInfo()!!.tbs)
TiebaApi.getInstance()
.unlikeForumFlow(forumId, forumName, AccountUtil.getLoginInfo()!!.tbs)
.map<CommonResponse, HomePartialChange.Unfollow> {
HomePartialChange.Unfollow.Success(forumId)
}
@ -109,6 +115,7 @@ sealed interface HomePartialChange : PartialChange<HomeUiState> {
topForums = oldState.topForums.filterNot { it.forumId == forumId },
)
}
is Failure -> oldState
}
@ -120,8 +127,14 @@ sealed interface HomePartialChange : PartialChange<HomeUiState> {
sealed class Refresh : HomePartialChange {
override fun reduce(oldState: HomeUiState): HomeUiState =
when (this) {
is Success -> oldState.copy(isLoading = false, forums = forums, topForums = topForums)
is Failure -> oldState.copy(isLoading = false)
is Success -> oldState.copy(
isLoading = false,
forums = forums,
topForums = topForums,
error = null
)
is Failure -> oldState.copy(isLoading = false, error = error)
Start -> oldState.copy(isLoading = true)
}
@ -133,7 +146,7 @@ sealed interface HomePartialChange : PartialChange<HomeUiState> {
) : Refresh()
data class Failure(
val errorMessage: String
val error: Throwable
) : Refresh()
}
@ -160,6 +173,7 @@ sealed interface HomePartialChange : PartialChange<HomeUiState> {
topForums = oldState.forums.filter { topForumsId.contains(it.forumId) }
)
}
is Failure -> oldState
}
@ -174,6 +188,7 @@ data class HomeUiState(
val isLoading: Boolean = true,
val forums: List<Forum> = emptyList(),
val topForums: List<Forum> = emptyList(),
val error: Throwable? = null,
) : UiState {
data class Forum(
val avatar: String,

View File

@ -0,0 +1,219 @@
package com.huanchengfly.tieba.post.ui.widgets.compose
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.MaterialTheme
import androidx.compose.material.ProvideTextStyle
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.LottieConstants
import com.airbnb.lottie.compose.rememberLottieComposition
import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.api.retrofit.exception.NoConnectivityException
import com.huanchengfly.tieba.post.api.retrofit.exception.TiebaApiException
import com.huanchengfly.tieba.post.api.retrofit.exception.getErrorCode
import com.huanchengfly.tieba.post.api.retrofit.exception.getErrorMessage
import com.huanchengfly.tieba.post.arch.BaseComposeActivity.Companion.LocalWindowSizeClass
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
import com.huanchengfly.tieba.post.ui.common.windowsizeclass.WindowWidthSizeClass
import com.huanchengfly.tieba.post.ui.widgets.compose.states.StateScreenScope
@Composable
fun TipScreen(
title: @Composable (ColumnScope.() -> Unit),
modifier: Modifier = Modifier,
image: @Composable (ColumnScope.() -> Unit) = {},
message: @Composable (ColumnScope.() -> Unit) = {},
actions: @Composable (ColumnScope.() -> Unit) = {}
) {
val widthFraction =
if (LocalWindowSizeClass.current.widthSizeClass == WindowWidthSizeClass.Compact) 0.9f else 0.5f
Column(modifier = modifier) {
Column(
modifier = Modifier
.fillMaxWidth(fraction = widthFraction)
.padding(16.dp)
.verticalScroll(
rememberScrollState()
),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.CenterVertically)
) {
image()
ProvideTextStyle(
value = MaterialTheme.typography.h6.copy(
color = ExtendedTheme.colors.text,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center
)
) {
title()
}
ProvideTextStyle(
value = MaterialTheme.typography.body1.copy(
color = ExtendedTheme.colors.textSecondary,
textAlign = TextAlign.Center
)
) {
message()
}
actions()
}
}
}
enum class ErrorType {
NETWORK,
SERVER,
UNKNOWN
}
@Composable
fun StateScreenScope.ErrorScreen(
error: Throwable,
modifier: Modifier = Modifier,
showReload: Boolean = true,
actions: @Composable (ColumnScope.() -> Unit) = {},
) {
ErrorTipScreen(
error = error,
modifier = modifier,
actions = {
if (showReload && canReload) {
Button(onClick = { reload() }) {
Text(text = stringResource(id = R.string.btn_reload))
}
}
actions()
}
)
}
@Composable
fun ErrorTipScreen(
error: Throwable,
modifier: Modifier = Modifier,
actions: @Composable (ColumnScope.() -> Unit) = {},
) {
val errorType = when (error) {
is NoConnectivityException -> ErrorType.NETWORK
is TiebaApiException -> ErrorType.SERVER
else -> ErrorType.UNKNOWN
}
val errorMessage = error.getErrorMessage()
val errorCode = error.getErrorCode()
ErrorTipScreen(
errorType = errorType,
errorMessage = errorMessage,
modifier = modifier,
errorCode = errorCode,
actions = actions
)
}
@Composable
fun ErrorTipScreen(
errorType: ErrorType,
errorMessage: String,
modifier: Modifier = Modifier,
errorCode: Int? = null,
appendMessage: @Composable (ColumnScope.() -> Unit) = {},
actions: @Composable (ColumnScope.() -> Unit) = {},
) {
TipScreen(
title = {
when (errorType) {
ErrorType.NETWORK -> {
Text(text = stringResource(id = R.string.title_no_internet_connectivity))
}
ErrorType.SERVER -> {
Text(text = stringResource(id = R.string.title_api_error))
}
ErrorType.UNKNOWN -> {
Text(text = stringResource(id = R.string.title_unknown_error))
}
}
},
modifier = modifier,
image = {
when (errorType) {
ErrorType.NETWORK -> {
val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.lottie_no_internet))
LottieAnimation(
composition = composition,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(2f)
)
}
ErrorType.SERVER -> {
val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.lottie_error))
LottieAnimation(
composition = composition,
iterations = LottieConstants.IterateForever,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(2f)
)
}
ErrorType.UNKNOWN -> {
val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.lottie_bug_hunting))
LottieAnimation(
composition = composition,
iterations = LottieConstants.IterateForever,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(2f)
)
}
}
},
message = {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
when (errorType) {
ErrorType.NETWORK -> {
Text(
text = stringResource(
id = R.string.message_no_internet_connectivity,
errorMessage
)
)
}
ErrorType.SERVER -> {
val errorCodeText = "($errorCode)".takeIf { errorCode != null }.orEmpty()
Text(text = "$errorMessage$errorCodeText")
}
ErrorType.UNKNOWN -> {
Text(text = stringResource(id = R.string.message_unknown_error))
}
}
appendMessage()
}
},
actions = actions
)
}

View File

@ -2,29 +2,42 @@ package com.huanchengfly.tieba.post.ui.widgets.compose.states
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.LottieConstants
import com.airbnb.lottie.compose.rememberLottieComposition
import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
import com.huanchengfly.tieba.post.ui.widgets.compose.EmptyPlaceholder
val DefaultLoadingScreen: @Composable () -> Unit = {
CircularProgressIndicator(modifier = Modifier.size(48.dp), color = MaterialTheme.colors.primary)
val DefaultLoadingScreen: @Composable StateScreenScope.() -> Unit = {
val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.lottie_loading_paperplane))
LottieAnimation(
composition = composition,
iterations = LottieConstants.IterateForever,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(2f)
)
// CircularProgressIndicator(modifier = Modifier.size(48.dp), color = MaterialTheme.colors.primary)
}
val DefaultEmptyScreen: @Composable () -> Unit = {
val DefaultEmptyScreen: @Composable StateScreenScope.() -> Unit = {
EmptyPlaceholder()
}
val DefaultErrorScreen: @Composable () -> Unit = {
val DefaultErrorScreen: @Composable StateScreenScope.() -> Unit = {
Text(
text = stringResource(id = R.string.error_tip),
style = MaterialTheme.typography.body1,
@ -39,12 +52,14 @@ fun StateScreen(
isLoading: Boolean,
modifier: Modifier = Modifier,
onReload: (() -> Unit)? = null,
emptyScreen: @Composable () -> Unit = DefaultEmptyScreen,
errorScreen: @Composable () -> Unit = DefaultErrorScreen,
loadingScreen: @Composable () -> Unit = DefaultLoadingScreen,
content: @Composable () -> Unit,
clickToReload: Boolean = false,
emptyScreen: @Composable StateScreenScope.() -> Unit = DefaultEmptyScreen,
errorScreen: @Composable StateScreenScope.() -> Unit = DefaultErrorScreen,
loadingScreen: @Composable StateScreenScope.() -> Unit = DefaultLoadingScreen,
content: @Composable StateScreenScope.() -> Unit,
) {
val clickableModifier = if (onReload != null) Modifier.clickable(
val stateScreenScope = remember(key1 = onReload) { StateScreenScope(onReload) }
val clickableModifier = if (onReload != null && clickToReload) Modifier.clickable(
enabled = isEmpty && !isLoading,
onClick = onReload
) else Modifier
@ -56,15 +71,26 @@ fun StateScreen(
contentAlignment = Alignment.Center
) {
if (!isEmpty) {
content()
stateScreenScope.content()
} else {
if (isLoading) {
loadingScreen()
stateScreenScope.loadingScreen()
} else if (isError) {
errorScreen()
stateScreenScope.errorScreen()
} else {
emptyScreen()
stateScreenScope.emptyScreen()
}
}
}
}
class StateScreenScope(
private val onReload: (() -> Unit)? = null
) {
val canReload: Boolean
get() = onReload != null
fun reload() {
onReload?.invoke()
}
}