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

问题描述 投票:0回答:3

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

// NetworkApi.kt

interface NetworkApi {
  @GET("users")
  suspend fun getUsers(): List<User>
}

在我的 ViewModel 中:

// MyViewModel.kt

class MyViewModel(private val networkApi: NetworkApi): ViewModel() {
  val usersLiveData = flow {
    emit(networkApi.getUsers())
  }.asLiveData()
}

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

//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
    }
  }
}

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

但是,我不确定如何使用这种方法优雅地处理网络错误。我能想到的一种方法是使用

Response<T>
作为网络 API 的返回类型,如下所示:

// NetworkApi.kt

interface NetworkApi {
  @GET("users")
  suspend fun getUsers(): Response<List<User>>
}

然后在我的视图模型中,我可以使用 if-else 来检查响应的

isSuccessful
,如果成功,则使用
.body()
API 获取真实结果。但是当我在视图模型中进行一些转换时就会出现问题。例如

// 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()

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

map
flatMapConcat
),那么每一步都这样做真的很烦人。

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

android kotlin retrofit kotlin-coroutines kotlin-flow
3个回答
15
投票

您应该有一个

sealed class
来处理不同类型的事件。例如,
Success
Error
Loading
。以下是一些适合您的用例的示例。

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
    )
}

然后,在您的 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()

在你的视图(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.  
      }
    }
 }

5
投票

//该类代表load语句管理操作 /*

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

ٍٍٍٍٍ

sealed class APIResponse<out T>{

    class Success<T>(response: Response<T>): APIResponse<T>() {
        val data = response.body()
    }


    class Failure<T>(response: Response<T>): APIResponse<T>() {
        val message:String = response.errorBody().toString()
    }


    class Exception<T>(throwable: Throwable): APIResponse<T>() {
        val message:String? = throwable.localizedMessage
    }


}

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

fun <T> APIResponse<T>.onSuccess(onResult :APIResponse.Success<T>.() -> Unit) : APIResponse<T>{
    if (this is APIResponse.Success) onResult(this)
    return this
}

fun <T> APIResponse<T>.onFailure(onResult: APIResponse.Failure<*>.() -> Unit) : APIResponse<T>{
    if (this is APIResponse.Failure<*>)
        onResult(this)
    return this
}

fun <T> APIResponse<T>.onException(onResult: APIResponse.Exception<*>.() -> Unit) : APIResponse<T>{
    if (this is APIResponse.Exception<*>) onResult(this)
    return this
}

将其与 Retrofit 合并

inline fun <T> Call<T>.request(crossinline onResult: (response: APIResponse<T>) -> Unit) {
    enqueue(object : retrofit2.Callback<T> {
        override fun onResponse(call: Call<T>, response: Response<T>) {
            if (response.isSuccessful) {
               // success
                onResult(APIResponse.Success(response))
            } else {
               //failure 
                onResult(APIResponse.Failure(response))
            }
        }

        override fun onFailure(call: Call<T>, throwable: Throwable) {
            onResult(APIResponse.Exception(throwable))
        }
    })
}

0
投票

对于本地化错误消息,您可以将应用程序上下文与 Dagger Hilt 结合使用。您还可以使用 int

R.string.message
并在 UI 级别执行
context.getString

sealed class Resource<T> {
    data class Success<T>(val data: T): Resource<T>()
    data class Error<T>(val message: String): Resource<T>()
    class Loading<T>: Resource<T>()
}
@Module
@InstallIn(SingletonComponent::class)
object AppModule {

    @Singleton
    @Provides
    fun provideRandomArticleRepository(
        api: WikipediaApi,
        @ApplicationContext appContext: Context
    ) = RandomArticleRepository(api, appContext)

    @Singleton
    @Provides
    fun provideWikipediaApi() = Retrofit.Builder()
        .addConverterFactory(GsonConverterFactory.create())
        .baseUrl(BASE_URL)
        .build()
        .create(WikipediaApi::class.java)
}
@ActivityScoped
class RandomArticleRepository @Inject constructor(
    private val api: WikipediaApi,
    private val context: Context
) {
    suspend fun getRandomArticle() = flow {
        emit(Resource.Loading())
        try {
            val response = api.getRandomArticle()
            emit(Resource.Success(response))
        } catch (e: Exception){
            emit(Resource.Error(e.localizedMessage ?: context.getString(R.string.unknown)))
        }
    }
}

然而,关闭Wifi时的消息只是

Error(message=Unable to resolve host "en.wikipedia.org": No address associated with hostname)
。 (检测到没有互联网连接

我尝试弄乱 URL,结果得到了

Error(message=HTTP 404 )
Error(message=Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 1 path $)

最重要的是,

e.localizedMessage
似乎没有本地化。 (用法语尝试过)要获得更全面的错误消息,您将被迫用
Response
@GET() suspend fun getRandomArticle(): Response<RandomArticleResponse>
包装所有内容才能访问错误代码。

© www.soinside.com 2019 - 2024. All rights reserved.