pref: 加载错误显示
This commit is contained in:
parent
addbc80bc0
commit
cf6c42ae1c
|
|
@ -6,7 +6,6 @@ import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
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.Avatar
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.Button
|
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.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.LongClickMenu
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.TextButton
|
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.Toolbar
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.accountNavIconIfCompact
|
import com.huanchengfly.tieba.post.ui.widgets.compose.accountNavIconIfCompact
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.rememberDialogState
|
import com.huanchengfly.tieba.post.ui.widgets.compose.rememberDialogState
|
||||||
|
|
@ -153,8 +154,8 @@ fun SearchBox(
|
||||||
@Composable
|
@Composable
|
||||||
private fun Header(
|
private fun Header(
|
||||||
text: String,
|
text: String,
|
||||||
invert: Boolean = false,
|
modifier: Modifier = Modifier,
|
||||||
modifier: Modifier = Modifier
|
invert: Boolean = false
|
||||||
) {
|
) {
|
||||||
Chip(
|
Chip(
|
||||||
text = text,
|
text = text,
|
||||||
|
|
@ -391,6 +392,11 @@ fun HomePage(
|
||||||
prop1 = HomeUiState::topForums,
|
prop1 = HomeUiState::topForums,
|
||||||
initial = emptyList()
|
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) }
|
var listSingle by remember { mutableStateOf(context.appPreferences.listSingle) }
|
||||||
val gridCells by remember { derivedStateOf { getGridCells(context, listSingle) } }
|
val gridCells by remember { derivedStateOf { getGridCells(context, listSingle) } }
|
||||||
|
|
||||||
|
|
@ -421,9 +427,12 @@ fun HomePage(
|
||||||
) { contentPaddings ->
|
) { contentPaddings ->
|
||||||
StateScreen(
|
StateScreen(
|
||||||
isEmpty = forums.isEmpty(),
|
isEmpty = forums.isEmpty(),
|
||||||
isError = false,
|
isError = isError,
|
||||||
isLoading = isLoading,
|
isLoading = isLoading,
|
||||||
modifier = Modifier.padding(contentPaddings),
|
modifier = Modifier.padding(contentPaddings),
|
||||||
|
onReload = {
|
||||||
|
viewModel.send(HomeUiIntent.Refresh)
|
||||||
|
},
|
||||||
emptyScreen = {
|
emptyScreen = {
|
||||||
EmptyScreen(
|
EmptyScreen(
|
||||||
loggedIn = account != null,
|
loggedIn = account != null,
|
||||||
|
|
@ -433,6 +442,9 @@ fun HomePage(
|
||||||
},
|
},
|
||||||
loadingScreen = {
|
loadingScreen = {
|
||||||
HomePageSkeletonScreen(listSingle = listSingle, gridCells = gridCells)
|
HomePageSkeletonScreen(listSingle = listSingle, gridCells = gridCells)
|
||||||
|
},
|
||||||
|
errorScreen = {
|
||||||
|
error?.let { ErrorScreen(error = it) }
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
val pullRefreshState = rememberPullRefreshState(
|
val pullRefreshState = rememberPullRefreshState(
|
||||||
|
|
@ -516,11 +528,11 @@ private fun HomePageSkeletonScreen(
|
||||||
Column {
|
Column {
|
||||||
Header(
|
Header(
|
||||||
text = stringResource(id = R.string.title_top_forum),
|
text = stringResource(id = R.string.title_top_forum),
|
||||||
invert = true,
|
|
||||||
modifier = Modifier.placeholder(
|
modifier = Modifier.placeholder(
|
||||||
visible = true,
|
visible = true,
|
||||||
color = ExtendedTheme.colors.chip
|
color = ExtendedTheme.colors.chip
|
||||||
)
|
),
|
||||||
|
invert = true
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
|
|
@ -541,11 +553,11 @@ private fun HomePageSkeletonScreen(
|
||||||
Column {
|
Column {
|
||||||
Header(
|
Header(
|
||||||
text = stringResource(id = R.string.forum_list_title),
|
text = stringResource(id = R.string.forum_list_title),
|
||||||
invert = true,
|
|
||||||
modifier = Modifier.placeholder(
|
modifier = Modifier.placeholder(
|
||||||
visible = true,
|
visible = true,
|
||||||
color = ExtendedTheme.colors.chip
|
color = ExtendedTheme.colors.chip
|
||||||
)
|
),
|
||||||
|
invert = true
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
|
|
@ -563,13 +575,15 @@ fun EmptyScreen(
|
||||||
onOpenExplore: () -> Unit
|
onOpenExplore: () -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
Column(
|
TipScreen(
|
||||||
modifier = Modifier
|
title = {
|
||||||
.fillMaxSize()
|
if (!loggedIn) {
|
||||||
.padding(16.dp),
|
Text(text = stringResource(id = R.string.title_empty_login))
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
} else {
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp, alignment = CenterVertically)
|
Text(text = stringResource(id = R.string.title_empty))
|
||||||
) {
|
}
|
||||||
|
},
|
||||||
|
image = {
|
||||||
val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.lottie_astronaut))
|
val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.lottie_astronaut))
|
||||||
LottieAnimation(
|
LottieAnimation(
|
||||||
composition = composition,
|
composition = composition,
|
||||||
|
|
@ -578,19 +592,19 @@ fun EmptyScreen(
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.aspectRatio(2f)
|
.aspectRatio(2f)
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
message = {
|
||||||
if (!loggedIn) {
|
if (!loggedIn) {
|
||||||
Text(
|
|
||||||
text = stringResource(id = R.string.title_empty_login),
|
|
||||||
style = MaterialTheme.typography.h6,
|
|
||||||
color = ExtendedTheme.colors.text,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
)
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = R.string.home_empty_login),
|
text = stringResource(id = R.string.home_empty_login),
|
||||||
style = MaterialTheme.typography.body1,
|
style = MaterialTheme.typography.body1,
|
||||||
color = ExtendedTheme.colors.textSecondary,
|
color = ExtendedTheme.colors.textSecondary,
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
if (!loggedIn) {
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
context.goToActivity<LoginActivity>()
|
context.goToActivity<LoginActivity>()
|
||||||
|
|
@ -600,13 +614,6 @@ fun EmptyScreen(
|
||||||
) {
|
) {
|
||||||
Text(text = stringResource(id = R.string.button_login))
|
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) {
|
if (canOpenExplore) {
|
||||||
TextButton(
|
TextButton(
|
||||||
|
|
@ -617,5 +624,6 @@ fun EmptyScreen(
|
||||||
Text(text = stringResource(id = R.string.button_go_to_explore))
|
Text(text = stringResource(id = R.string.button_go_to_explore))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -15,24 +15,29 @@ import org.litepal.LitePal
|
||||||
class HomeViewModel : BaseViewModel<HomeUiIntent, HomePartialChange, HomeUiState, HomeUiEvent>() {
|
class HomeViewModel : BaseViewModel<HomeUiIntent, HomePartialChange, HomeUiState, HomeUiEvent>() {
|
||||||
override fun createInitialState(): HomeUiState = HomeUiState()
|
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? =
|
override fun dispatchEvent(partialChange: HomePartialChange): UiEvent? =
|
||||||
when (partialChange) {
|
when (partialChange) {
|
||||||
is HomePartialChange.Refresh.Failure -> CommonUiEvent.Toast(partialChange.errorMessage)
|
|
||||||
is HomePartialChange.TopForums.Delete.Failure -> CommonUiEvent.Toast(partialChange.errorMessage)
|
is HomePartialChange.TopForums.Delete.Failure -> CommonUiEvent.Toast(partialChange.errorMessage)
|
||||||
is HomePartialChange.TopForums.Add.Failure -> CommonUiEvent.Toast(partialChange.errorMessage)
|
is HomePartialChange.TopForums.Add.Failure -> CommonUiEvent.Toast(partialChange.errorMessage)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
object HomePartialChangeProducer : PartialChangeProducer<HomeUiIntent, HomePartialChange, HomeUiState> {
|
object HomePartialChangeProducer :
|
||||||
|
PartialChangeProducer<HomeUiIntent, HomePartialChange, HomeUiState> {
|
||||||
@OptIn(FlowPreview::class)
|
@OptIn(FlowPreview::class)
|
||||||
override fun toPartialChangeFlow(intentFlow: Flow<HomeUiIntent>): Flow<HomePartialChange> {
|
override fun toPartialChangeFlow(intentFlow: Flow<HomeUiIntent>): Flow<HomePartialChange> {
|
||||||
return merge(
|
return merge(
|
||||||
intentFlow.filterIsInstance<HomeUiIntent.Refresh>().flatMapConcat { produceRefreshPartialChangeFlow() },
|
intentFlow.filterIsInstance<HomeUiIntent.Refresh>()
|
||||||
intentFlow.filterIsInstance<HomeUiIntent.TopForums.Delete>().flatMapConcat { it.toPartialChangeFlow() },
|
.flatMapConcat { produceRefreshPartialChangeFlow() },
|
||||||
intentFlow.filterIsInstance<HomeUiIntent.TopForums.Add>().flatMapConcat { it.toPartialChangeFlow() },
|
intentFlow.filterIsInstance<HomeUiIntent.TopForums.Delete>()
|
||||||
intentFlow.filterIsInstance<HomeUiIntent.Unfollow>().flatMapConcat { it.toPartialChangeFlow() },
|
.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)
|
HomePartialChange.Refresh.Success(forums, topForums)
|
||||||
}
|
}
|
||||||
.onStart { emit(HomePartialChange.Refresh.Start) }
|
.onStart { emit(HomePartialChange.Refresh.Start) }
|
||||||
.catch { emit(HomePartialChange.Refresh.Failure(it.getErrorMessage())) }
|
.catch { emit(HomePartialChange.Refresh.Failure(it)) }
|
||||||
|
|
||||||
private fun HomeUiIntent.TopForums.Delete.toPartialChangeFlow() =
|
private fun HomeUiIntent.TopForums.Delete.toPartialChangeFlow() =
|
||||||
flow {
|
flow {
|
||||||
|
|
@ -79,7 +84,8 @@ class HomeViewModel : BaseViewModel<HomeUiIntent, HomePartialChange, HomeUiState
|
||||||
.catch { emit(HomePartialChange.TopForums.Add.Failure(it.getErrorMessage())) }
|
.catch { emit(HomePartialChange.TopForums.Add.Failure(it.getErrorMessage())) }
|
||||||
|
|
||||||
private fun HomeUiIntent.Unfollow.toPartialChangeFlow() =
|
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> {
|
.map<CommonResponse, HomePartialChange.Unfollow> {
|
||||||
HomePartialChange.Unfollow.Success(forumId)
|
HomePartialChange.Unfollow.Success(forumId)
|
||||||
}
|
}
|
||||||
|
|
@ -109,6 +115,7 @@ sealed interface HomePartialChange : PartialChange<HomeUiState> {
|
||||||
topForums = oldState.topForums.filterNot { it.forumId == forumId },
|
topForums = oldState.topForums.filterNot { it.forumId == forumId },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is Failure -> oldState
|
is Failure -> oldState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -120,8 +127,14 @@ sealed interface HomePartialChange : PartialChange<HomeUiState> {
|
||||||
sealed class Refresh : HomePartialChange {
|
sealed class Refresh : HomePartialChange {
|
||||||
override fun reduce(oldState: HomeUiState): HomeUiState =
|
override fun reduce(oldState: HomeUiState): HomeUiState =
|
||||||
when (this) {
|
when (this) {
|
||||||
is Success -> oldState.copy(isLoading = false, forums = forums, topForums = topForums)
|
is Success -> oldState.copy(
|
||||||
is Failure -> oldState.copy(isLoading = false)
|
isLoading = false,
|
||||||
|
forums = forums,
|
||||||
|
topForums = topForums,
|
||||||
|
error = null
|
||||||
|
)
|
||||||
|
|
||||||
|
is Failure -> oldState.copy(isLoading = false, error = error)
|
||||||
Start -> oldState.copy(isLoading = true)
|
Start -> oldState.copy(isLoading = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -133,7 +146,7 @@ sealed interface HomePartialChange : PartialChange<HomeUiState> {
|
||||||
) : Refresh()
|
) : Refresh()
|
||||||
|
|
||||||
data class Failure(
|
data class Failure(
|
||||||
val errorMessage: String
|
val error: Throwable
|
||||||
) : Refresh()
|
) : Refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -160,6 +173,7 @@ sealed interface HomePartialChange : PartialChange<HomeUiState> {
|
||||||
topForums = oldState.forums.filter { topForumsId.contains(it.forumId) }
|
topForums = oldState.forums.filter { topForumsId.contains(it.forumId) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is Failure -> oldState
|
is Failure -> oldState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -174,6 +188,7 @@ data class HomeUiState(
|
||||||
val isLoading: Boolean = true,
|
val isLoading: Boolean = true,
|
||||||
val forums: List<Forum> = emptyList(),
|
val forums: List<Forum> = emptyList(),
|
||||||
val topForums: List<Forum> = emptyList(),
|
val topForums: List<Forum> = emptyList(),
|
||||||
|
val error: Throwable? = null,
|
||||||
) : UiState {
|
) : UiState {
|
||||||
data class Forum(
|
data class Forum(
|
||||||
val avatar: String,
|
val avatar: String,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -2,29 +2,42 @@ package com.huanchengfly.tieba.post.ui.widgets.compose.states
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.material.CircularProgressIndicator
|
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
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.R
|
||||||
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
|
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.EmptyPlaceholder
|
import com.huanchengfly.tieba.post.ui.widgets.compose.EmptyPlaceholder
|
||||||
|
|
||||||
val DefaultLoadingScreen: @Composable () -> Unit = {
|
val DefaultLoadingScreen: @Composable StateScreenScope.() -> Unit = {
|
||||||
CircularProgressIndicator(modifier = Modifier.size(48.dp), color = MaterialTheme.colors.primary)
|
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()
|
EmptyPlaceholder()
|
||||||
}
|
}
|
||||||
|
|
||||||
val DefaultErrorScreen: @Composable () -> Unit = {
|
val DefaultErrorScreen: @Composable StateScreenScope.() -> Unit = {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = R.string.error_tip),
|
text = stringResource(id = R.string.error_tip),
|
||||||
style = MaterialTheme.typography.body1,
|
style = MaterialTheme.typography.body1,
|
||||||
|
|
@ -39,12 +52,14 @@ fun StateScreen(
|
||||||
isLoading: Boolean,
|
isLoading: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onReload: (() -> Unit)? = null,
|
onReload: (() -> Unit)? = null,
|
||||||
emptyScreen: @Composable () -> Unit = DefaultEmptyScreen,
|
clickToReload: Boolean = false,
|
||||||
errorScreen: @Composable () -> Unit = DefaultErrorScreen,
|
emptyScreen: @Composable StateScreenScope.() -> Unit = DefaultEmptyScreen,
|
||||||
loadingScreen: @Composable () -> Unit = DefaultLoadingScreen,
|
errorScreen: @Composable StateScreenScope.() -> Unit = DefaultErrorScreen,
|
||||||
content: @Composable () -> Unit,
|
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,
|
enabled = isEmpty && !isLoading,
|
||||||
onClick = onReload
|
onClick = onReload
|
||||||
) else Modifier
|
) else Modifier
|
||||||
|
|
@ -56,15 +71,26 @@ fun StateScreen(
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
if (!isEmpty) {
|
if (!isEmpty) {
|
||||||
content()
|
stateScreenScope.content()
|
||||||
} else {
|
} else {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
loadingScreen()
|
stateScreenScope.loadingScreen()
|
||||||
} else if (isError) {
|
} else if (isError) {
|
||||||
errorScreen()
|
stateScreenScope.errorScreen()
|
||||||
} else {
|
} else {
|
||||||
emptyScreen()
|
stateScreenScope.emptyScreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class StateScreenScope(
|
||||||
|
private val onReload: (() -> Unit)? = null
|
||||||
|
) {
|
||||||
|
val canReload: Boolean
|
||||||
|
get() = onReload != null
|
||||||
|
|
||||||
|
fun reload() {
|
||||||
|
onReload?.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue