基于Android架构组件与MVVM的错误处理方案咨询
Hey there! Handling error flow from your repository up to the View layer is a key part of making your app robust—let’s walk through a clean, scalable pattern that fits right into the Jetpack architecture you’re using.
1. Define a Sealed Class for State Wrapping
First, we need a consistent way to represent successful results, loading states, and errors. A sealed class is perfect here because it enforces exhaustive handling in your View layer:
sealed class Resource<out T> { // For successful data retrieval data class Success<out T>(val data: T) : Resource<T>() // For errors, carrying the message to display data class Error(val errorMessage: String) : Resource<Nothing>() // For loading states (optional but super useful for UX) object Loading : Resource<Nothing>() }
2. Handle Errors in the Repository
Your repository is where you’ll catch network exceptions, parse error responses, and wrap everything into the Resource class. This keeps all data-fetching logic centralized:
class UserRepository(private val apiService: UserApiService) { suspend fun fetchUser(userId: Int): Resource<User> { return try { val response = apiService.getUser(userId) if (response.isSuccessful) { // Return success if we have valid data response.body()?.let { Resource.Success(it) } ?: Resource.Error("Failed to load user: Empty response") } else { // Handle HTTP errors (4xx, 5xx) Resource.Error("Request failed: ${response.message()}") } } catch (e: IOException) { // Catch network-related issues (no internet, timeouts) Resource.Error("Network error: ${e.localizedMessage ?: "Check your connection"}") } catch (e: Exception) { // Catch-all for unexpected errors Resource.Error("Oops! Something went wrong: ${e.localizedMessage}") } } }
3. Propagate State to ViewModel
In your ViewModel, you’ll call the repository method and expose the state to the View layer using a StateFlow (or LiveData if you prefer). Use viewModelScope to handle coroutines safely:
class UserViewModel(private val repository: UserRepository) : ViewModel() { // Mutable state for internal updates private val _userState = MutableStateFlow<Resource<User>>(Resource.Loading) // Immutable state exposed to the View val userState: StateFlow<Resource<User>> = _userState.asStateFlow() fun loadUser(userId: Int) { viewModelScope.launch { _userState.value = Resource.Loading // Show loading first val result = repository.fetchUser(userId) _userState.value = result // Update with success or error } } // Optional: Add a retry method if you want users to refresh fun retryLoadUser(userId: Int) { loadUser(userId) } }
4. Observe State and Show Errors in the View
Finally, in your Activity or Fragment, observe the ViewModel’s state and update the UI accordingly. Use repeatOnLifecycle to ensure you only collect state when the View is active:
class UserProfileFragment : Fragment() { private val viewModel: UserViewModel by viewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // Observe the user state lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.userState.collect { state -> when (state) { is Resource.Loading -> showLoadingIndicator(true) is Resource.Success -> { showLoadingIndicator(false) displayUserProfile(state.data) } is Resource.Error -> { showLoadingIndicator(false) showErrorMessage(state.errorMessage) } } } } } // Trigger initial load viewModel.loadUser(123) } private fun showLoadingIndicator(isLoading: Boolean) { // Toggle visibility of your progress bar binding.progressBar.isVisible = isLoading } private fun displayUserProfile(user: User) { // Populate your UI with user data binding.userName.text = user.name binding.userEmail.text = user.email } private fun showErrorMessage(message: String) { // Show error to the user—Snackbar, Toast, or an error TextView Snackbar.make(requireView(), message, Snackbar.LENGTH_LONG) .setAction("Retry") { viewModel.retryLoadUser(123) } .show() } }
Quick Best Practices
- Localize Error Messages: Instead of passing raw strings, use string resource IDs (e.g.,
R.string.network_error) so your app supports multiple languages. - Granular Error Types: Extend the
Resource.Errorclass into subclasses (likeNetworkError,AuthError) if you need to handle specific errors differently (e.g., redirect to login for auth issues). - Debounce Retries: If adding retry logic, consider adding a delay or limiting retries to avoid spamming the network.
内容的提问来源于stack exchange,提问作者Hayk Galstyan

