Add comprehensive error handling utilities for all screens

Created reusable error handling components that can be used across all
screens in both Android and iOS apps to show retry/cancel dialogs when
network calls fail.

Android Components:
- ApiResultHandler: Composable that automatically handles ApiResult states
  with loading indicators and error dialogs
- HandleErrors(): Extension function for ApiResult to show error dialogs
  for operations that don't return display data
- Updated ResidencesScreen to import ApiResultHandler

iOS Components:
- ViewStateHandler: SwiftUI view that handles loading/error/success states
  with automatic error alerts
- handleErrors(): View modifier for automatic error monitoring
- Both use the existing ErrorAlertModifier for consistent alerts

Documentation:
- Created ERROR_HANDLING.md with comprehensive usage guide
- Includes examples for data loading and create/update/delete operations
- Migration guide for updating existing screens
- Best practices and testing guidelines

These utilities make it easy to add consistent error handling with retry
functionality to any screen that makes network calls, improving the user
experience across the entire app.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-11-14 14:28:09 -06:00
parent 0e3b9681f6
commit 1d3a06f492
4 changed files with 285 additions and 98 deletions

View File

@@ -0,0 +1,146 @@
package com.mycrib.android.ui.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.mycrib.shared.network.ApiResult
/**
* Handles ApiResult states automatically with loading, error dialogs, and success content.
*
* Example usage:
* ```
* val state by viewModel.dataState.collectAsState()
*
* ApiResultHandler(
* state = state,
* onRetry = { viewModel.loadData() }
* ) { data ->
* // Success content using the data
* Text("Data: ${data.name}")
* }
* ```
*
* @param T The type of data in the ApiResult.Success
* @param state The current ApiResult state
* @param onRetry Callback to retry the operation when error occurs
* @param modifier Modifier for the container
* @param loadingContent Custom loading content (default: CircularProgressIndicator)
* @param errorTitle Custom error dialog title (default: "Network Error")
* @param content Content to show when state is Success
*/
@Composable
fun <T> ApiResultHandler(
state: ApiResult<T>,
onRetry: () -> Unit,
modifier: Modifier = Modifier,
loadingContent: @Composable (() -> Unit)? = null,
errorTitle: String = "Network Error",
content: @Composable (T) -> Unit
) {
var showErrorDialog by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf("") }
// Show error dialog when state changes to Error
LaunchedEffect(state) {
if (state is ApiResult.Error) {
errorMessage = state.message
showErrorDialog = true
}
}
when (state) {
is ApiResult.Idle, is ApiResult.Loading -> {
Box(
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
if (loadingContent != null) {
loadingContent()
} else {
CircularProgressIndicator()
}
}
}
is ApiResult.Error -> {
// Show loading indicator while error dialog is displayed
Box(
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
is ApiResult.Success -> {
content(state.data)
}
}
// Error dialog
if (showErrorDialog) {
ErrorDialog(
title = errorTitle,
message = errorMessage,
onRetry = {
showErrorDialog = false
onRetry()
},
onDismiss = {
showErrorDialog = false
}
)
}
}
/**
* Extension function to observe ApiResult state and show error dialog
* Use this for operations that don't return data to display (like create/update/delete)
*
* Example usage:
* ```
* val createState by viewModel.createState.collectAsState()
*
* createState.HandleErrors(
* onRetry = { viewModel.createItem() }
* )
*
* LaunchedEffect(createState) {
* if (createState is ApiResult.Success) {
* // Handle success
* navController.popBackStack()
* }
* }
* ```
*/
@Composable
fun <T> ApiResult<T>.HandleErrors(
onRetry: () -> Unit,
errorTitle: String = "Network Error"
) {
var showErrorDialog by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf("") }
LaunchedEffect(this) {
if (this@HandleErrors is ApiResult.Error) {
errorMessage = (this@HandleErrors as ApiResult.Error).message
showErrorDialog = true
}
}
if (showErrorDialog) {
ErrorDialog(
title = errorTitle,
message = errorMessage,
onRetry = {
showErrorDialog = false
onRetry()
},
onDismiss = {
showErrorDialog = false
}
)
}
}

View File

@@ -20,6 +20,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.mycrib.android.ui.components.ApiResultHandler
import com.mycrib.android.ui.components.JoinResidenceDialog
import com.mycrib.android.ui.components.common.StatItem
import com.mycrib.android.ui.components.residence.TaskStatChip