Skip to content

Commit 70b622a

Browse files
jkmasselclaude
andcommitted
Show error and empty states in Android example app
Add shared ErrorMessage and EmptyState composables with a WpRequestResult.errorDescription() extension for human-readable messages. Surface API errors and empty-but-successful results across all 16 screens via a new error StateFlow in each view model. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent addde68 commit 70b622a

33 files changed

+391
-38
lines changed

native/kotlin/example/composeApp/src/commonMain/kotlin/rs/wordpress/example/shared/ui/applicationpasswords/ApplicationPasswordListScreen.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import androidx.compose.runtime.remember
2020
import androidx.compose.ui.Modifier
2121
import org.jetbrains.compose.ui.tooling.preview.Preview
2222
import rs.wordpress.api.kotlin.WpApiClient
23+
import rs.wordpress.example.shared.ui.components.EmptyState
24+
import rs.wordpress.example.shared.ui.components.ErrorMessage
2325
import rs.wordpress.example.shared.ui.components.LoadingIndicator
2426

2527
@OptIn(ExperimentalMaterial3Api::class)
@@ -32,6 +34,7 @@ fun ApplicationPasswordListScreen(
3234
) {
3335
val applicationPasswords by viewModel.applicationPasswords.collectAsState()
3436
val isLoading by viewModel.isLoading.collectAsState()
37+
val error by viewModel.error.collectAsState()
3538

3639
Scaffold(
3740
topBar = {
@@ -47,6 +50,10 @@ fun ApplicationPasswordListScreen(
4750
) { paddingValues ->
4851
if (isLoading) {
4952
LoadingIndicator(modifier = Modifier.padding(paddingValues))
53+
} else if (error != null) {
54+
ErrorMessage(error!!, modifier = Modifier.padding(paddingValues))
55+
} else if (applicationPasswords.isEmpty()) {
56+
EmptyState("No application passwords found", modifier = Modifier.padding(paddingValues))
5057
} else {
5158
LazyColumn(
5259
modifier = Modifier.fillMaxSize().padding(paddingValues)

native/kotlin/example/composeApp/src/commonMain/kotlin/rs/wordpress/example/shared/ui/applicationpasswords/ApplicationPasswordListViewModel.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.asStateFlow
99
import kotlinx.coroutines.launch
1010
import rs.wordpress.api.kotlin.WpApiClient
1111
import rs.wordpress.api.kotlin.WpRequestResult
12+
import rs.wordpress.example.shared.ui.components.errorDescription
1213
import uniffi.wp_api.ApplicationPasswordWithEditContext
1314

1415
class ApplicationPasswordListViewModel(private val apiClient: WpApiClient) {
@@ -20,6 +21,9 @@ class ApplicationPasswordListViewModel(private val apiClient: WpApiClient) {
2021
private val _isLoading = MutableStateFlow(true)
2122
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
2223

24+
private val _error = MutableStateFlow<String?>(null)
25+
val error: StateFlow<String?> = _error.asStateFlow()
26+
2327
init {
2428
loadApplicationPasswords()
2529
}
@@ -32,6 +36,7 @@ class ApplicationPasswordListViewModel(private val apiClient: WpApiClient) {
3236
val userId = when (meResult) {
3337
is WpRequestResult.Success -> meResult.response.data.id
3438
else -> {
39+
_error.value = meResult.errorDescription()
3540
_isLoading.value = false
3641
return@launch
3742
}
@@ -42,7 +47,10 @@ class ApplicationPasswordListViewModel(private val apiClient: WpApiClient) {
4247
}
4348
when (result) {
4449
is WpRequestResult.Success -> _applicationPasswords.value = result.response.data
45-
else -> _applicationPasswords.value = emptyList()
50+
else -> {
51+
_error.value = result.errorDescription()
52+
_applicationPasswords.value = emptyList()
53+
}
4654
}
4755
_isLoading.value = false
4856
}

native/kotlin/example/composeApp/src/commonMain/kotlin/rs/wordpress/example/shared/ui/comments/CommentListScreen.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import androidx.compose.runtime.remember
2323
import androidx.compose.ui.Modifier
2424
import org.jetbrains.compose.ui.tooling.preview.Preview
2525
import rs.wordpress.api.kotlin.WpApiClient
26+
import rs.wordpress.example.shared.ui.components.EmptyState
27+
import rs.wordpress.example.shared.ui.components.ErrorMessage
2628
import rs.wordpress.example.shared.ui.components.LoadingIndicator
2729
import rs.wordpress.example.shared.ui.components.LoadingMoreIndicator
2830

@@ -38,6 +40,7 @@ fun CommentListScreen(
3840
val isLoading by viewModel.isLoading.collectAsState()
3941
val isLoadingMore by viewModel.isLoadingMore.collectAsState()
4042
val canLoadMore by viewModel.canLoadMore.collectAsState()
43+
val error by viewModel.error.collectAsState()
4144

4245
val listState = rememberLazyListState()
4346
val shouldLoadMore by remember {
@@ -68,6 +71,10 @@ fun CommentListScreen(
6871
) { paddingValues ->
6972
if (isLoading) {
7073
LoadingIndicator(modifier = Modifier.padding(paddingValues))
74+
} else if (error != null && comments.isEmpty()) {
75+
ErrorMessage(error!!, modifier = Modifier.padding(paddingValues))
76+
} else if (comments.isEmpty()) {
77+
EmptyState("No comments found", modifier = Modifier.padding(paddingValues))
7178
} else {
7279
LazyColumn(
7380
state = listState,
@@ -82,7 +89,11 @@ fun CommentListScreen(
8289
overlineContent = { Text(comment.status.toString()) }
8390
)
8491
}
85-
if (isLoadingMore) {
92+
if (error != null) {
93+
item {
94+
ErrorMessage(error!!)
95+
}
96+
} else if (isLoadingMore) {
8697
item { LoadingMoreIndicator() }
8798
}
8899
}

native/kotlin/example/composeApp/src/commonMain/kotlin/rs/wordpress/example/shared/ui/comments/CommentListViewModel.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.asStateFlow
99
import kotlinx.coroutines.launch
1010
import rs.wordpress.api.kotlin.WpApiClient
1111
import rs.wordpress.api.kotlin.WpRequestResult
12+
import rs.wordpress.example.shared.ui.components.errorDescription
1213
import uniffi.wp_api.CommentListParams
1314
import uniffi.wp_api.CommentWithEditContext
1415

@@ -18,6 +19,9 @@ class CommentListViewModel(private val apiClient: WpApiClient) {
1819
private val _comments = MutableStateFlow<List<CommentWithEditContext>>(emptyList())
1920
val comments: StateFlow<List<CommentWithEditContext>> = _comments.asStateFlow()
2021

22+
private val _error = MutableStateFlow<String?>(null)
23+
val error: StateFlow<String?> = _error.asStateFlow()
24+
2125
private val _isLoading = MutableStateFlow(true)
2226
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
2327

@@ -44,7 +48,10 @@ class CommentListViewModel(private val apiClient: WpApiClient) {
4448
nextPageParams = result.response.nextPageParams
4549
_canLoadMore.value = nextPageParams != null
4650
}
47-
else -> _comments.value = emptyList()
51+
else -> {
52+
_error.value = result.errorDescription()
53+
_comments.value = emptyList()
54+
}
4855
}
4956
_isLoading.value = false
5057
}
@@ -64,7 +71,7 @@ class CommentListViewModel(private val apiClient: WpApiClient) {
6471
nextPageParams = result.response.nextPageParams
6572
_canLoadMore.value = nextPageParams != null
6673
}
67-
else -> {}
74+
else -> _error.value = result.errorDescription()
6875
}
6976
_isLoadingMore.value = false
7077
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package rs.wordpress.example.shared.ui.components
2+
3+
import androidx.compose.foundation.layout.Box
4+
import androidx.compose.foundation.layout.fillMaxSize
5+
import androidx.compose.foundation.layout.padding
6+
import androidx.compose.material3.MaterialTheme
7+
import androidx.compose.material3.Text
8+
import androidx.compose.runtime.Composable
9+
import androidx.compose.ui.Alignment
10+
import androidx.compose.ui.Modifier
11+
import androidx.compose.ui.unit.dp
12+
import rs.wordpress.api.kotlin.WpRequestResult
13+
import uniffi.wp_api.RequestExecutionErrorReason
14+
import uniffi.wp_api.WpErrorCode
15+
16+
fun <T> WpRequestResult<T>.errorDescription(): String = when (this) {
17+
is WpRequestResult.Success -> ""
18+
is WpRequestResult.WpError ->
19+
"$errorMessage (${errorCode.displayName()})\n$requestMethod $requestUrl"
20+
is WpRequestResult.RequestExecutionFailed ->
21+
"${reason.description()}\n$requestMethod $requestUrl"
22+
is WpRequestResult.InvalidHttpStatusCode ->
23+
"Unexpected HTTP status: $statusCode\n$requestMethod $requestUrl"
24+
is WpRequestResult.ResponseParsingError ->
25+
"Failed to parse response: $reason\n$requestMethod $requestUrl"
26+
is WpRequestResult.SiteUrlParsingError -> "Invalid site URL: $reason"
27+
is WpRequestResult.MediaFileNotFound -> "File not found: $filePath"
28+
is WpRequestResult.UnknownError ->
29+
"Unknown error (HTTP $statusCode)\n$requestMethod $requestUrl"
30+
}
31+
32+
private fun WpErrorCode.displayName(): String = when (this) {
33+
is WpErrorCode.CustomException -> v1
34+
else -> this::class.simpleName ?: "Unknown"
35+
}
36+
37+
private fun RequestExecutionErrorReason.description(): String = when (this) {
38+
is RequestExecutionErrorReason.DeviceIsOfflineError -> "Device is offline: $errorMessage"
39+
is RequestExecutionErrorReason.HttpTimeoutError -> "Request timed out"
40+
is RequestExecutionErrorReason.InvalidSslError -> "SSL error: $reason"
41+
is RequestExecutionErrorReason.NonExistentSiteError -> errorMessage ?: "Site not found"
42+
is RequestExecutionErrorReason.HttpAuthenticationRequiredError -> "Authentication required for $hostname"
43+
is RequestExecutionErrorReason.HttpAuthenticationRejectedError -> "Authentication rejected for $hostname"
44+
is RequestExecutionErrorReason.HttpForbiddenError -> "Access forbidden for $hostname"
45+
is RequestExecutionErrorReason.MisconfiguredHttpAuthenticationError -> "HTTP authentication misconfigured: $issue"
46+
is RequestExecutionErrorReason.MisconfiguredRateLimitError -> "Rate limit misconfigured"
47+
is RequestExecutionErrorReason.CancellationError -> "Request cancelled"
48+
is RequestExecutionErrorReason.HttpError -> "HTTP error: $reason"
49+
is RequestExecutionErrorReason.GenericError -> errorMessage
50+
}
51+
52+
@Composable
53+
fun ErrorMessage(message: String, modifier: Modifier = Modifier) {
54+
Box(
55+
modifier = modifier.fillMaxSize().padding(16.dp),
56+
contentAlignment = Alignment.Center
57+
) {
58+
Text(
59+
text = message,
60+
color = MaterialTheme.colorScheme.error,
61+
style = MaterialTheme.typography.bodyLarge
62+
)
63+
}
64+
}
65+
66+
@Composable
67+
fun EmptyState(message: String, modifier: Modifier = Modifier) {
68+
Box(
69+
modifier = modifier.fillMaxSize().padding(16.dp),
70+
contentAlignment = Alignment.Center
71+
) {
72+
Text(
73+
text = message,
74+
color = MaterialTheme.colorScheme.onSurfaceVariant,
75+
style = MaterialTheme.typography.bodyLarge
76+
)
77+
}
78+
}

native/kotlin/example/composeApp/src/commonMain/kotlin/rs/wordpress/example/shared/ui/media/MediaListScreen.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import androidx.compose.runtime.remember
2323
import androidx.compose.ui.Modifier
2424
import org.jetbrains.compose.ui.tooling.preview.Preview
2525
import rs.wordpress.api.kotlin.WpApiClient
26+
import rs.wordpress.example.shared.ui.components.EmptyState
27+
import rs.wordpress.example.shared.ui.components.ErrorMessage
2628
import rs.wordpress.example.shared.ui.components.LoadingIndicator
2729
import rs.wordpress.example.shared.ui.components.LoadingMoreIndicator
2830

@@ -38,6 +40,7 @@ fun MediaListScreen(
3840
val isLoading by viewModel.isLoading.collectAsState()
3941
val isLoadingMore by viewModel.isLoadingMore.collectAsState()
4042
val canLoadMore by viewModel.canLoadMore.collectAsState()
43+
val error by viewModel.error.collectAsState()
4144

4245
val listState = rememberLazyListState()
4346
val shouldLoadMore by remember {
@@ -68,6 +71,10 @@ fun MediaListScreen(
6871
) { paddingValues ->
6972
if (isLoading) {
7073
LoadingIndicator(modifier = Modifier.padding(paddingValues))
74+
} else if (error != null && media.isEmpty()) {
75+
ErrorMessage(error!!, modifier = Modifier.padding(paddingValues))
76+
} else if (media.isEmpty()) {
77+
EmptyState("No media found", modifier = Modifier.padding(paddingValues))
7178
} else {
7279
LazyColumn(
7380
state = listState,
@@ -80,7 +87,11 @@ fun MediaListScreen(
8087
overlineContent = { Text(item.sourceUrl) }
8188
)
8289
}
83-
if (isLoadingMore) {
90+
if (error != null) {
91+
item {
92+
ErrorMessage(error!!)
93+
}
94+
} else if (isLoadingMore) {
8495
item { LoadingMoreIndicator() }
8596
}
8697
}

native/kotlin/example/composeApp/src/commonMain/kotlin/rs/wordpress/example/shared/ui/media/MediaListViewModel.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.asStateFlow
99
import kotlinx.coroutines.launch
1010
import rs.wordpress.api.kotlin.WpApiClient
1111
import rs.wordpress.api.kotlin.WpRequestResult
12+
import rs.wordpress.example.shared.ui.components.errorDescription
1213
import uniffi.wp_api.MediaListParams
1314
import uniffi.wp_api.MediaWithEditContext
1415

@@ -18,6 +19,9 @@ class MediaListViewModel(private val apiClient: WpApiClient) {
1819
private val _media = MutableStateFlow<List<MediaWithEditContext>>(emptyList())
1920
val media: StateFlow<List<MediaWithEditContext>> = _media.asStateFlow()
2021

22+
private val _error = MutableStateFlow<String?>(null)
23+
val error: StateFlow<String?> = _error.asStateFlow()
24+
2125
private val _isLoading = MutableStateFlow(true)
2226
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
2327

@@ -44,7 +48,10 @@ class MediaListViewModel(private val apiClient: WpApiClient) {
4448
nextPageParams = result.response.nextPageParams
4549
_canLoadMore.value = nextPageParams != null
4650
}
47-
else -> _media.value = emptyList()
51+
else -> {
52+
_error.value = result.errorDescription()
53+
_media.value = emptyList()
54+
}
4855
}
4956
_isLoading.value = false
5057
}
@@ -64,7 +71,7 @@ class MediaListViewModel(private val apiClient: WpApiClient) {
6471
nextPageParams = result.response.nextPageParams
6572
_canLoadMore.value = nextPageParams != null
6673
}
67-
else -> {}
74+
else -> _error.value = result.errorDescription()
6875
}
6976
_isLoadingMore.value = false
7077
}

native/kotlin/example/composeApp/src/commonMain/kotlin/rs/wordpress/example/shared/ui/menulocations/MenuLocationListScreen.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import androidx.compose.runtime.remember
2020
import androidx.compose.ui.Modifier
2121
import org.jetbrains.compose.ui.tooling.preview.Preview
2222
import rs.wordpress.api.kotlin.WpApiClient
23+
import rs.wordpress.example.shared.ui.components.EmptyState
24+
import rs.wordpress.example.shared.ui.components.ErrorMessage
2325
import rs.wordpress.example.shared.ui.components.LoadingIndicator
2426

2527
@OptIn(ExperimentalMaterial3Api::class)
@@ -32,6 +34,7 @@ fun MenuLocationListScreen(
3234
) {
3335
val menuLocations by viewModel.menuLocations.collectAsState()
3436
val isLoading by viewModel.isLoading.collectAsState()
37+
val error by viewModel.error.collectAsState()
3538

3639
Scaffold(
3740
topBar = {
@@ -47,6 +50,10 @@ fun MenuLocationListScreen(
4750
) { paddingValues ->
4851
if (isLoading) {
4952
LoadingIndicator(modifier = Modifier.padding(paddingValues))
53+
} else if (error != null) {
54+
ErrorMessage(error!!, modifier = Modifier.padding(paddingValues))
55+
} else if (menuLocations.isEmpty()) {
56+
EmptyState("No menu locations found", modifier = Modifier.padding(paddingValues))
5057
} else {
5158
LazyColumn(
5259
modifier = Modifier.fillMaxSize().padding(paddingValues)

native/kotlin/example/composeApp/src/commonMain/kotlin/rs/wordpress/example/shared/ui/menulocations/MenuLocationListViewModel.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.asStateFlow
99
import kotlinx.coroutines.launch
1010
import rs.wordpress.api.kotlin.WpApiClient
1111
import rs.wordpress.api.kotlin.WpRequestResult
12+
import rs.wordpress.example.shared.ui.components.errorDescription
1213
import uniffi.wp_api.MenuLocationWithEditContext
1314

1415
class MenuLocationListViewModel(private val apiClient: WpApiClient) {
@@ -20,6 +21,9 @@ class MenuLocationListViewModel(private val apiClient: WpApiClient) {
2021
private val _isLoading = MutableStateFlow(true)
2122
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
2223

24+
private val _error = MutableStateFlow<String?>(null)
25+
val error: StateFlow<String?> = _error.asStateFlow()
26+
2327
init {
2428
loadMenuLocations()
2529
}
@@ -33,7 +37,10 @@ class MenuLocationListViewModel(private val apiClient: WpApiClient) {
3337
is WpRequestResult.Success -> {
3438
_menuLocations.value = result.response.data.locations.values.toList()
3539
}
36-
else -> _menuLocations.value = emptyList()
40+
else -> {
41+
_error.value = result.errorDescription()
42+
_menuLocations.value = emptyList()
43+
}
3744
}
3845
_isLoading.value = false
3946
}

0 commit comments

Comments
 (0)