使用 Retrofit + Kotlin Flow 处理错误的优雅方式

see*_*uch 15 android kotlin retrofit kotlin-coroutines kotlin-flow

我最喜欢在 Android 上执行网络请求的方法(使用 Retrofit)。它看起来像这样:

// NetworkApi.kt

interface NetworkApi {
  @GET("users")
  suspend fun getUsers(): List<User>
}
Run Code Online (Sandbox Code Playgroud)

在我的 ViewModel 中:

// MyViewModel.kt

class MyViewModel(private val networkApi: NetworkApi): ViewModel() {
  val usersLiveData = flow {
    emit(networkApi.getUsers())
  }.asLiveData()
}
Run Code Online (Sandbox Code Playgroud)

最后,在我的活动/片段中:

//MyActivity.kt

class MyActivity: AppCompatActivity() {
  private viewModel: MyViewModel by viewModels()

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    viewModel.usersLiveData.observe(this) {
      // Update the UI here
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

我喜欢这种方式的原因是因为它本身就可以与 Kotlin flow 配合使用,非常易于使用,并且有很多有用的操作(flatMap 等)。

但是,我不确定如何使用此方法优雅地处理网络错误。我能想到的一种方法是用作Response<T>网络 API 的返回类型,如下所示:

// NetworkApi.kt

interface NetworkApi {
  @GET("users")
  suspend fun getUsers(): Response<List<User>>
}
Run Code Online (Sandbox Code Playgroud)

然后在我的视图模型中,我可以使用 if-else 来检查响应,并在成功时isSuccessful使用 API 获取真实结果。.body()但是当我在视图模型中进行一些转换时就会出现问题。例如

// MyViewModel.kt
class MyViewModel(private val networkApi: NetworkApi): ViewModel() {
  val usersLiveData = flow {
    val response = networkApi.getUsers()
    if (response.isSuccessful) {
      emit(response.body()) // response.body() will be List<User>
    } else {
       // What should I do here?
    }
  }.map { // it: List<User>
    // transform Users to some other class
    it?.map { oneUser -> OtherClass(oneUser.userName) }
  }.asLiveData()
Run Code Online (Sandbox Code Playgroud)

请注意评论“我应该在这里做什么?”。我不知道在这种情况下该怎么办。我可以用一些“状态”包装响应主体(在本例中为用户列表)(或者只是简单地传递响应本身)。但这意味着我几乎必须使用 if-else 来检查流转换链中每一步的状态,一直到 UI。如果链条真的很长(比如我有10条map或者flatMapConcat上链),每一步都要做真的很烦人。

请问在这种情况下处理网络错误的最佳方法是什么?

Teo*_*Teo 15

您应该能够sealed class处理不同类型的事件。例如,SuccessErrorLoading。以下是一些适合您的用例的示例。

enum class ApiStatus{
    SUCCESS,
    ERROR,
    LOADING
}  // for your case might be simplify to use only sealed class

sealed class ApiResult <out T> (val status: ApiStatus, val data: T?, val message:String?) {

    data class Success<out R>(val _data: R?): ApiResult<R>(
        status = ApiStatus.SUCCESS,
        data = _data,
        message = null
    )

    data class Error(val exception: String): ApiResult<Nothing>(
        status = ApiStatus.ERROR,
        data = null,
        message = exception
    )

    data class Loading<out R>(val _data: R?, val isLoading: Boolean): ApiResult<R>(
        status = ApiStatus.LOADING,
        data = _data,
        message = null
    )
}
Run Code Online (Sandbox Code Playgroud)

然后,在您的 ViewModel 中,

class MyViewModel(private val networkApi: NetworkApi): ViewModel() {

  // this should be returned as a function, not a variable
  val usersLiveData = flow {
    emit(ApiResult.Loading(true))   // 1. Loading State
    val response = networkApi.getUsers()
    if (response.isSuccessful) {
      emit(ApiResult.Success(response.body()))   // 2. Success State
    } else {
       val errorMsg = response.errorBody()?.string()
       response.errorBody()?.close()  // remember to close it after getting the stream of error body
       emit(ApiResult.Error(errorMsg))  // 3. Error State
    }
  }.map { // it: List<User>
    // transform Users to some other class
    it?.map { oneUser -> OtherClass(oneUser.userName) }
  }.asLiveData()
Run Code Online (Sandbox Code Playgroud)

在你的视图(Activity/Fragment)中,观察这些状态。

 viewModel.usersLiveData.observe(this) { result ->
      // Update the UI here
    when(result.status) {
      ApiResult.Success ->  {
         val data = result.data  <-- return List<User>
      }
      ApiResult.Error ->   {
         val errorMsg = result.message  <-- return errorBody().string()
      }
      ApiResult.Loading ->  {
         // here will actually set the state as Loading 
         // you may put your loading indicator here.  
      }
    }
 }
Run Code Online (Sandbox Code Playgroud)


omr*_*awy 5

//该类代表加载语句管理操作\n/*

\n
    \n
  • 什么是密封类
  • \n
  • 密封类是具有受限类层次结构的抽象类。
  • \n
  • 从它继承的类必须与密封类位于同一文件中。
  • \n
  • 这提供了对继承的更多控制。它们受到限制,但也允许国家代表自由。
  • \n
  • \n
  • 密封类可以嵌套数据类、类、对象以及其他密封类。
  • \n
  • 在处理其他密封类时,自动完成功能会发挥作用。
  • \n
  • 这是因为 IDE 可以检测这些类中的分支。
  • \n
  • */
  • \n
\n

\xd9\x8d\xd9\x8d\xd9\x8d\xd9\x8d\xd9\x8d

\n
sealed class APIResponse<out T>{\n\n    class Success<T>(response: Response<T>): APIResponse<T>() {\n        val data = response.body()\n    }\n\n\n    class Failure<T>(response: Response<T>): APIResponse<T>() {\n        val message:String = response.errorBody().toString()\n    }\n\n\n    class Exception<T>(throwable: Throwable): APIResponse<T>() {\n        val message:String? = throwable.localizedMessage\n    }\n\n\n}\n
Run Code Online (Sandbox Code Playgroud)\n

创建名为 APIResponsrEX.kt\n的扩展文件并创建扩展方法

\n
fun <T> APIResponse<T>.onSuccess(onResult :APIResponse.Success<T>.() -> Unit) : APIResponse<T>{\n    if (this is APIResponse.Success) onResult(this)\n    return this\n}\n\nfun <T> APIResponse<T>.onFailure(onResult: APIResponse.Failure<*>.() -> Unit) : APIResponse<T>{\n    if (this is APIResponse.Failure<*>)\n        onResult(this)\n    return this\n}\n\nfun <T> APIResponse<T>.onException(onResult: APIResponse.Exception<*>.() -> Unit) : APIResponse<T>{\n    if (this is APIResponse.Exception<*>) onResult(this)\n    return this\n}\n
Run Code Online (Sandbox Code Playgroud)\n

将其与 Retrofit 合并

\n
inline fun <T> Call<T>.request(crossinline onResult: (response: APIResponse<T>) -> Unit) {\n    enqueue(object : retrofit2.Callback<T> {\n        override fun onResponse(call: Call<T>, response: Response<T>) {\n            if (response.isSuccessful) {\n               // success\n                onResult(APIResponse.Success(response))\n            } else {\n               //failure \n                onResult(APIResponse.Failure(response))\n            }\n        }\n\n        override fun onFailure(call: Call<T>, throwable: Throwable) {\n            onResult(APIResponse.Exception(throwable))\n        }\n    })\n}\n
Run Code Online (Sandbox Code Playgroud)\n