Kotlin 使用协程处理改造请求

Posted

技术标签:

【中文标题】Kotlin 使用协程处理改造请求【英文标题】:Kotlin handle retrofit request with coroutines 【发布时间】:2021-05-21 05:31:02 【问题描述】:

我正在制作一个 android 应用程序,我正在尝试登录。我提出了一个基本的改装请求并且它有效,但我想用一个通用类处理来自服务器的响应,以向用户显示错误(例如电子邮件/密码错误)。 我按照本教程https://blog.mindorks.com/using-retrofit-with-kotlin-coroutines-in-android 进行操作,但在这里他在 viewModel 中提出请求并访问存储在 mainActivity(视图类)的 Resource 中的数据。我想访问 viewModel 中的数据以在共享首选项中保存一些信息(查看第一个代码块中的注释),但我不知道该怎么做。 有人可以解释一下如何更改代码以从 ViewModel 访问 Resource 中的 data 吗? 这是我的视图模型:

class LoginViewModel(private val loginRepo: LoginRepository) : ViewModel() 
private fun makeLogin(email: String, password: String) 
        viewModelScope.launch 
            Resource.loading(data = null)
            try 

                val usr = User(email, password)
                Resource.success(data = loginRepo.makeLogin(usr))
                // HERE I WANT TO ACCESS TO DATA TO STORE SOME INFORMATION IN SHARED PREFERENCES
             catch (ex: Exception) 
                Resource.error(data = null, message = ex.message ?: "Error occured!")
            

这里是资源类:

data class Resource<out T>(val status: Status, val data: T?, val message: String?) 
    companion object 
        fun <T> success(data: T): Resource<T> = Resource(status = Status.SUCCESS, data = data, message = null)

        fun <T> error(data: T?, message: String): Resource<T> =
            Resource(status = Status.ERROR, data = data, message = message)

        fun <T> loading(data: T?): Resource<T> = Resource(status = Status.LOADING, data = data, message = null)
    

这里是存储库:

class LoginRepository(private val apiHelper: ApiHelper) 
    suspend fun makeLogin(usr: User) = apiHelper.makeLogin(usr)

apiHelper.makeLogin(usr)的返回类型是:


@JsonClass(generateAdapter = true)
data class LoginResponse(
    val token: String,
    val expiration: String,
    val id : Int,
    val role: Int)

教程的viewModel

class MainViewModel(private val mainRepository: MainRepository) : ViewModel() 

    fun getUsers() = liveData(Dispatchers.IO) 
        emit(Resource.loading(data = null))
        try 
            emit(Resource.success(data = mainRepository.getUsers()))
         catch (exception: Exception) 
            emit(Resource.error(data = null, message = exception.message ?: "Error Occurred!"))
        
    

在教程中他在主活动中访问存储在 Resource 中的数据,如下所示:

viewModel.getUsers().observe(this, Observer 
            it?.let  resource ->
                when (resource.status) 
                    SUCCESS -> 
                        recyclerView.visibility = View.VISIBLE
                        progressBar.visibility = View.GONE
                        resource.data?.let  users -> retrieveList(users) 
                    
                    ERROR -> 
                        recyclerView.visibility = View.VISIBLE
                        progressBar.visibility = View.GONE
                        Toast.makeText(this, it.message, Toast.LENGTH_LONG).show()
                    
                    LOADING -> 
                        progressBar.visibility = View.VISIBLE
                        recyclerView.visibility = View.GONE
                    
                
            
        )

【问题讨论】:

apiHelper.makeLogin(usr) 的返回类型是什么? 我提出问题 能否也给这个方法添加定义viewModel.getUsers() 我提出了问题,您可以在我发布的链接中找到教程的完整代码@iCantC 【参考方案1】:

我猜你可以直接将响应分配给一个变量并访问它,同时将它传递给Resource

private fun makeLogin(email: String, password: String) 
    viewModelScope.launch 
        Resource.loading(data = null)
        try 
            val usr = User(email, password)
            val loginResponse = loginRepo.makeLogin(usr)
            Resource.success(data = loginResponse)
            //you can access loginResponse to access data inside it             
         catch (ex: Exception) 
             Resource.error(data = null, message = ex.message ?: "Error occured!")
        
    

【讨论】:

这是一个好习惯吗?您对如何为我提出的所有请求实现一个通用类有了更好的了解【参考方案2】:

您可以这样做:

class LoginViewModel(private val loginRepo: LoginRepository) : ViewModel() 
private fun makeLogin(email: String, password: String) 
    viewModelScope.launch 
        Resource.loading(data = null)
        try 

            val usr = User(email, password)
            val response = loginRepo.makeLogin(usr)
            Resource.success(data = response)
            // HERE YOU HAVE ACCESS TO RESPONSE TO DO WHATEVER YOU WANT WITH IT
         catch (ex: Exception) 
            Resource.error(data = null, message = ex.message ?: "Error occured!")
        

【讨论】:

但这很好吗?在这种情况下是不是没用的资源类? @azziza432 重点是,您不应直接访问 ViewModel 中的 android 资源(如共享首选项)。无论如何,如果您可以完全在 ViewModel 中处理您的响应,那么资源类就没有用处。资源类是 ViewModel 和 View 之间通信的一种方式。 那么我应该在哪里保存共享首选项? 我正在尝试使用 MVVM 架构实现最佳方法,但我是初学者 我建议你在你的存储库中这样做。存储库是存储或获取数据的地方。因此,在收到您的数据后,将其传递给您的存储库的函数以存储信息。【参考方案3】:

为所有请求处理对象类型的响应,然后根据需要将该对象分配给您的模型类,或者按照您想要的方式解析对象

【讨论】:

【参考方案4】:

在我看来,loading 状态不是响应状态,而是视图状态,因此我更愿意避免使用无用的Loading class 来跟踪调用的加载状态。如果你使用协程,我猜你知道调用何时处于loading 状态,因为你正在执行一个挂起函数。

因此,对于这个问题,我觉得有用的是为响应定义一个通用的sealed class,其类型可以是SuccessError

sealed class Result<out R> 

    data class Success<out T>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()

    override fun toString(): String 
        return when (this) 
            is Success<*> -> "Success[data=$data]"
            is Error -> "Error[exception=$exception]"
        
    

然后我在我的数据源中使用这个类,返回一个Result.Success(带有它的数据)或一个Result.Error(带有它的异常消息)

override suspend fun getCities(): Result<List<City>> = withContext(Dispatchers.IO) 
        try 
            val response = service.getCities()
            if (response.isSuccessful) 
                val result = Result.Success(response.body()!!.cities)
                return@withContext result
             else 
                return@withContext Result.Error(Exception(Exceptions.SERVER_ERROR))
            
         catch (e: Exception) 
            return@withContext Result.Error(e)
        
    

ViewModel 中,我只是为视图设置了一个“加载状态”observable,并在调用挂起函数之前和之后发布了该可观察对象的更新:

class ForecastsViewModel @ViewModelInject constructor(
    private val citiesRepository: CitiesRepository) : ViewModel() 
    
    private val _dataLoading = MutableLiveData(false)
    val dataLoading: LiveData<Boolean> = _dataLoading
    
    private val _error = MutableLiveData<String>()
    val error: LiveData<String> = _error

    private val _cities = MutableLiveData<List<City>>()

    val cities: LiveData<List<City>> = _cities
    
    // The view calls this method and observes dataLoading to change state
    fun loadCities() 
            viewModelScope.launch 
                _dataLoading.value = true
                when (val result = citiesRepository.getCities(true)) 
                    is Result.Success -> 
                        citiesDownloaded.postValue(true)
                    
                    is Result.Error -> 
                        _error.postValue(result.exception.message)
                    
                
                _dataLoading.value = false
            
    


如果您想深入了解代码,请查看我的 github 回购关于此主题的信息

【讨论】:

以上是关于Kotlin 使用协程处理改造请求的主要内容,如果未能解决你的问题,请参考以下文章

PublishSubject 与 Kotlin 协程(流/通道)

发现不一样的Kotlin多方位处理协程的异常

发现不一样的Kotlin多方位处理协程的异常

如何使用 Kotlin Coroutines 在 Retrofit 中处理 204 响应?

如何使用 kotlin 协程处理回调

Kotlin协程-并发处理-基础