Reputation: 25
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
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
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
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
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
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