如何在 Android 中处理 API 响应(成功/错误)?
可能有很多方法可以处理来自 android 中的服务器的 API 响应,但是您是否使用了一种好的方法来处理它?在本文中,我们将看到在使用 Retrofit 库进行 API 调用时在 Kotlin Sealed Class 的帮助下处理 API 响应。这种密封类方法非常适合这种情况:如果用户期待 UI 中的数据,那么我们需要将错误一直发送到我们的 UI 以通知用户并确保用户不会只看到空白屏幕或遇到意外的 UI。
制作密封类
Note: We are following MVVM Architecture and using Retrofit with Kotlin Coroutines for api calls in background.
只需在数据包或 utils/others 包的单独文件中创建一个名为 Resource 的通用密封类。我们将其命名为 Resource 并创建它为泛型,因为我们将使用这个类来包装我们不同类型的 API 响应。基本上,我们需要在这三个事件上更新我们的 UI,即 Success、Error、Loading。因此,我们将它们创建为 Resources 的子类来表示 UI 的不同状态。现在,如果成功,我们将获取数据,因此我们将使用 Resource 包装它。成功,如果出现错误,我们将使用 Resource 包装错误消息。错误&在加载的情况下,我们只会返回 Resource.Loading 对象(您也可以根据需要更改它以包装加载数据)。
Kotlin
sealed class Resource(
val data: T? = null,
val message: String? = null
) {
// We'll wrap our data in this 'Success'
// class in case of success response from api
class Success(data: T) : Resource(data = data)
// We'll pass error message wrapped in this 'Error'
// class to the UI in case of failure response
class Error(errorMessage: String) : Resource(message = errorMessage)
// We'll just pass object of this Loading
// class, just before making an api call
class Loading : Resource()
}
Kotlin
interface ExampleApiService {
@GET("example/popular_articles")
suspend fun fetchPopularArticles(): Response
// We have wrapped our api response in
// Retrofit's Response class. So that we can have
// some extra information about api call response
// like weather it succeed or not, etc
@GET("example/new_articles")
suspend fun fetchNewArticles(): Response
}
Kotlin
abstract class BaseRepo() {
// we'll use this function in all
// repos to handle api errors.
suspend fun safeApiCall(apiToBeCalled: suspend () -> Response): Resource {
// Returning api response
// wrapped in Resource class
return withContext(Dispatchers.IO) {
try {
// Here we are calling api lambda
// function that will return response
// wrapped in Retrofit's Response class
val response: Response = apiToBeCalled()
if (response.isSuccessful) {
// In case of success response we
// are returning Resource.Success object
// by passing our data in it.
Resource.Success(data = response.body()!!)
} else {
// parsing api's own custom json error
// response in ExampleErrorResponse pojo
val errorResponse: ExampleErrorResponse? = convertErrorBody(response.errorBody())
// Simply returning api's own failure message
Resource.Error(errorMessage = errorResponse?.failureMessage ?: "Something went wrong")
}
} catch (e: HttpException) {
// Returning HttpException's message
// wrapped in Resource.Error
Resource.Error(errorMessage = e.message ?: "Something went wrong")
} catch (e: IOException) {
// Returning no internet message
// wrapped in Resource.Error
Resource.Error("Please check your network connection")
} catch (e: Exception) {
// Returning 'Something went wrong' in case
// of unknown error wrapped in Resource.Error
Resource.Error(errorMessage = "Something went wrong")
}
}
}
// If you don't wanna handle api's own
// custom error response then ignore this function
private fun convertErrorBody(errorBody: ResponseBody?): ExampleErrorResponse? {
return try {
errorBody?.source()?.let {
val moshiAdapter = Moshi.Builder().build().adapter(ExampleErrorResponse::class.java)
moshiAdapter.fromJson(it)
}
} catch (exception: Exception) {
null
}
}
}
Kotlin
class ExampleRepo(private val apiService: ExampleApiService) : BaseRepo() {
suspend fun getPopularArticles() : Resource {
// Passing 'api.fetchPopularArticles()' function
// as an argument in safeApiCall function
return safeApiCall { apiService.fetchPopularArticles() }
}
suspend fun getPublishedArticles() : Resource = safeApiCall { apiService.fetchPublishedArticles() }
}
Kotlin
class ExampleViewModel (private val exampleRepo: ExampleRepo) {
private val _popularArticles = MutableLiveData>()
val popularArticles: LiveData> = _popularArticles
init {
// Calling this function in init{}
// block so that it'll automatically
// get called on initialization of viewmodel
getPopularArticles()
}
private fun getPopularArticles() = viewModelScope.launch {
// Firstly we are posting
// Loading state in mutableLiveData
_popularArticles.postValue(Resource.Loading())
// Posting success response
// as it becomes ready
_popularArticles.postValue(exampleRepo.getPopularArticles())
}
}
让我们也看看我们的 ApiService 接口(见下文),在每个暂停的 API 调用函数中,我们使用 Retrofit 的 Response 类包装我们的 API 调用响应,因为它为我们提供了一些关于 API 调用的额外信息。例如:调用是否成功,错误代码等。我们将从我们的存储库中调用这些挂起的函数......我们会在稍后看到。不要对我们自定义的 Resource 类和 Retrofit 的 Response 类感到困惑。 Resource 类仅代表 UI 的不同状态,而 Retrofit 的 Response 类为我们提供了有关 API 调用的额外信息。
科特林
interface ExampleApiService {
@GET("example/popular_articles")
suspend fun fetchPopularArticles(): Response
// We have wrapped our api response in
// Retrofit's Response class. So that we can have
// some extra information about api call response
// like weather it succeed or not, etc
@GET("example/new_articles")
suspend fun fetchNewArticles(): Response
}
现在让我们跳到 API 成功/错误处理的主要部分
您必须根据需要在项目中拥有不同的存储库,并且所有 api 调用必须通过这些存储库进行,我们需要对每个 API 调用进行错误处理。因此,我们需要将每个 API 调用包装在 try-catch 块中。但是等等……为每个 api 调用编写一个 try-catch 块并不是一个好主意。因此,让我们创建一个 BaseRepository 并在其中编写一个名为 safeApiCall(您喜欢的任何名称)的通用挂起函数,该函数将负责处理每个和每个 api 调用的 api 响应。
科特林
abstract class BaseRepo() {
// we'll use this function in all
// repos to handle api errors.
suspend fun safeApiCall(apiToBeCalled: suspend () -> Response): Resource {
// Returning api response
// wrapped in Resource class
return withContext(Dispatchers.IO) {
try {
// Here we are calling api lambda
// function that will return response
// wrapped in Retrofit's Response class
val response: Response = apiToBeCalled()
if (response.isSuccessful) {
// In case of success response we
// are returning Resource.Success object
// by passing our data in it.
Resource.Success(data = response.body()!!)
} else {
// parsing api's own custom json error
// response in ExampleErrorResponse pojo
val errorResponse: ExampleErrorResponse? = convertErrorBody(response.errorBody())
// Simply returning api's own failure message
Resource.Error(errorMessage = errorResponse?.failureMessage ?: "Something went wrong")
}
} catch (e: HttpException) {
// Returning HttpException's message
// wrapped in Resource.Error
Resource.Error(errorMessage = e.message ?: "Something went wrong")
} catch (e: IOException) {
// Returning no internet message
// wrapped in Resource.Error
Resource.Error("Please check your network connection")
} catch (e: Exception) {
// Returning 'Something went wrong' in case
// of unknown error wrapped in Resource.Error
Resource.Error(errorMessage = "Something went wrong")
}
}
}
// If you don't wanna handle api's own
// custom error response then ignore this function
private fun convertErrorBody(errorBody: ResponseBody?): ExampleErrorResponse? {
return try {
errorBody?.source()?.let {
val moshiAdapter = Moshi.Builder().build().adapter(ExampleErrorResponse::class.java)
moshiAdapter.fromJson(it)
}
} catch (exception: Exception) {
null
}
}
}
safeApiCall 有一个名为 'api' 的暂停 lambda函数,它返回包装在 Resource 中的数据,以便 safeApiCall 能够接收我们的每个 api 调用函数。由于 safeApiCall 是一个挂起函数,所以我们将 withContext 协程范围与 Dispatchers.IO 一起使用,这样所有的网络调用都将在 Input/Output 后台线程上运行。每当网络调用失败时,它都会引发异常,因此我们需要在 try-catch 块中调用我们的 lambda函数'apiToBeCalled',如果 api 调用成功,那么我们将在 Resource 中包装成功响应数据。成功对象 & 我们将通过 safeApiCall 返回它,如果 api 调用失败,那么我们将在以下 catch 块之一中捕获该特定异常:
- 当处理 HTTP 请求时服务器出现问题时,它会抛出 HTTP 异常。
- 当用户没有有效的互联网/wifi 连接时,它会抛出Java IO 异常。
- 如果发生任何其他异常,那么它将简单地出现在一般异常块中(将该块放在最后)。您还可以根据您的具体情况在最后一个通用异常捕获块之前添加其他异常捕获块。
现在在 catch 块中,我们可以简单地发送我们自己的自定义错误消息或从 Resource.Error 对象中的异常获取的错误消息。
Note: In case if network call doesn’t fail & doesn’t throw an exception instead if it is sending it’s own custom error json response (sometimes happens in case of Authentication failure/Token expires/Invalid api key/etc) then we won’t get an exception instead Retrofit’s Response class will help us determine by returning true/false on response.is Successful. In the above code also we are handling that inside try block, if it returns false then inside else case we’ll parse that api’s own custom error json response (see example below) into a pojo class created by us i.e. ExampleErrorResponse. For parsing this json response to our ExampleErrorResponse pojo, we are using Moshi library in our convertErrorBody function that’ll return ExampleErrorResponse object & then we can get API’s custom error response.
{
“status”: ”UnSuccessful”,
“failure_message” : ”Invalid api key or api key expires”
}
因此,最终我们的 safeApiCall函数将返回包装在 Resource 类中的响应。它可能是成功或错误。如果您已经了解了这么多,那么请向您大声喊叫。你赢得了大部分的战斗。
现在让我们更改在当前存储库中调用 APIs 的方式
我们将从 BaseRepo 扩展我们的所有存储库,以便我们可以在那里访问 safeApiCall函数。在我们的 repo 函数中,例如 getPopularArticles,我们将 ApiService 的函数(例如:apiService.fetchPopularArticles())作为 lambda 参数传递给 safeApiCall,然后 safeApiCall 将简单地处理其成功/错误部分,然后最终它将返回 Resource
Note: Remember when we’ll call getPopularArticles function in viewmodel (see example below) then first we’ll post Resource.Loading object in mutable live data such that our fragment/activity can observe this loading state and can update the UI to show loading state and then as we’ll get our api response ready, it’ll be posted in mutable live data again so that UI can get this response (success/error) update again and can show data in UI or update itself accordingly.
科特林
class ExampleRepo(private val apiService: ExampleApiService) : BaseRepo() {
suspend fun getPopularArticles() : Resource {
// Passing 'api.fetchPopularArticles()' function
// as an argument in safeApiCall function
return safeApiCall { apiService.fetchPopularArticles() }
}
suspend fun getPublishedArticles() : Resource = safeApiCall { apiService.fetchPublishedArticles() }
}
视图模型
科特林
class ExampleViewModel (private val exampleRepo: ExampleRepo) {
private val _popularArticles = MutableLiveData>()
val popularArticles: LiveData> = _popularArticles
init {
// Calling this function in init{}
// block so that it'll automatically
// get called on initialization of viewmodel
getPopularArticles()
}
private fun getPopularArticles() = viewModelScope.launch {
// Firstly we are posting
// Loading state in mutableLiveData
_popularArticles.postValue(Resource.Loading())
// Posting success response
// as it becomes ready
_popularArticles.postValue(exampleRepo.getPopularArticles())
}
}
就是这样,现在我们可以轻松地在我们的 UI 中观察所有状态,即成功、错误、正在加载。那是关于以更好的方式处理 api 错误处理或 api 响应处理。