azziza432
azziza432

Reputation: 25

Kotlin handle retrofit request with coroutines

I'm making an Android app and i'm trying to make a login. I made a basic retofit request and it works, but i want to handle the response from the server with a generic class, to show error to the user (for example email/password wrong). I follow this tutorial https://blog.mindorks.com/using-retrofit-with-kotlin-coroutines-in-android but here he make the request in the viewModel and access to the data stored in Resource in the mainActivity (the view class). I want to access data in the viewModel to save some information in shared preferences (look the comment in the first code block), but I don't know how to do this. Can someone explain me how to change the code to have access to data in Resource from the ViewModel? Here is my viewModel:

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

here is the resource class:

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

here is the Repository:

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

The return type of apiHelper.makeLogin(usr) is:


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

The viewModel of the tutorial:

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

In the tutorial he access the data stored in Resource in the main activity like this:

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

Upvotes: 0

Views: 4553

Answers (5)

Hmerman6006
Hmerman6006

Reputation: 1931

Had to figure out the same thing on how to get the response in ViewModel and send data to Activity if no http error occurred. Keep it simple and keep everything as is except:

1> Wrap your expected data class return in your service class and/or repository with retrofit2.Response<*>. Just replace asterisk with expected return.
2> Change your ViewModel code:

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

    fun getUsers() = liveData(Dispatchers.IO) {
        emit(Resource.loading(data = null))
        try {
             val res: Response<*> = mainRepository.getUsers() // * Add data class Output expected
             res?.let {response: Response<*> -> // * Same
                 if (response.isSuccessful) {
                     // TODO: Add code to set sharedpreferences
                     emit(Resource.success(data = response)) // return response or response.body()
                 } else {
                     throw retrofit2.HttpException(response)
                 }
             }
        } catch (exception: Exception) {
            emit(Resource.error(data = null, message = exception.message ?: "Error Occurred!"))
        }
    }
}

Upvotes: 0

Nicola Gallazzi
Nicola Gallazzi

Reputation: 8713

In my opinion the loading state is not a response state, is a state of the view, so I prefer avoiding to put a useless Loading class to keep track of the loading state of the call. If you are using coroutines, as I guess, you know when a call is in a loading state, because you're executing a suspending function.

So, for this problem I find useful is to define a generic sealed class for responses, which can be of type Success or Error

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]"
        }
    }
}

Then I use this class in my data source, returning a Result.Success (with its data) or a Result.Error (with its exception message)

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

In the ViewModel I simply have a "loading state" observable for the view, and I post updates on that observable before and after calling the suspending function:

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

}

If you want to go deep with the code, check my github repo about this topic

Upvotes: 4

Vishal Saini
Vishal Saini

Reputation: 1

Handle your response in object type for all requests then assign that object to your model class if you want or parse the object as the way you want

Upvotes: 0

Pouya Heydari
Pouya Heydari

Reputation: 2614

You can do that simply by:

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

Upvotes: 0

Rajan Kali
Rajan Kali

Reputation: 12953

I guess you can directly assign response to a variable and access it also while sending passing it to 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!")
        }
    }
}

Upvotes: 0

Related Questions