Problems with handling network response - android

I have some problem with handling network response. Loading and Success state work perfectly fine but Error state doesn't work. When i simulate an error then triggered Loading and Success state not an Error. My code below.
Repository class
fun getMoviesStream(genreId: String?, sortBy: String): Flow<PagingData<Result>> {
return Pager(
config = PagingConfig(pageSize = MOVIES_PAGE_SIZE, enablePlaceholders = false),
pagingSourceFactory = { MoviesPagingSource(api, genreId, sortBy) }
).flow
}
ViewModel class
private val _moviesResponse = MutableStateFlow<Resource<PagingData<Result>>>(Resource.Loading())
val moviesResponse = _moviesResponse.asStateFlow()
fun getMovies(genreId: String?, sortBy: String) {
viewModelScope.launch {
_moviesResponse.value = Resource.Loading()
moviesRepository.getMoviesStream(genreId, sortBy).cachedIn(viewModelScope)
.catch { throwable ->
_moviesResponse.value = Resource.Error(
throwable.localizedMessage ?: "An unexpected error occurred"
)
}
.collect { result ->
delay(500)
_moviesResponse.value = Resource.Success(result)
}
}
}
Fragment class
private fun requestMoviesData(genreId: String?, sortBy: String) {
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
moviesViewModel.getMovies(genreId, sortBy)
moviesViewModel.moviesResponse.collect { response ->
when (response) {
is Resource.Error -> {
Toast.makeText(
requireContext(),
response.message,
Toast.LENGTH_SHORT
).show()
}
is Resource.Loading -> {
binding.spinnerLoading.visibility = View.VISIBLE
}
is Resource.Success -> {
binding.spinnerLoading.visibility = View.GONE
response.data?.let { moviesAdapter.submitData(it) }
}
}
}
}
}
}

Please check the flow catch operator documentation.
https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/catch.html
This operator is transparent to exceptions that occur in downstream flow and does not catch exceptions that are thrown to cancel the flow.

Related

Composable visibility not changing on State change

I have a isLoading state and I'm trying to show a CircularProgressIndicator when the value is true.
#Composable
fun ProductDetailScreen(
viewModel: ProductDetailViewModel = hiltViewModel()
) {
val productState = viewModel.productState.value
LazyColumn{
item {
if (productState.isLoading)
CircularProgressIndicator()
}
}
}
I'm using a Resource class for my API call results and in the repository I use this class to wrap my request result.
The problem is, although I'm returning Resource.Loading from the repository, the isLoading state is not being updated from ViewModel and the ProgressIndicator is not shown in my screen. What could be causing this behavior?
sealed class Resource<T>(
val data: T? = null,
val message: String? = null,
val errorType: ExceptionMapper.Type? = null
) {
class Success<T>(data: T?) : Resource<T>(data)
class Error<T>(message: String, errorType: ExceptionMapper.Type, data: T? = null) : Resource<T>(data, message, errorType)
class Loading<T>(isLoading: Boolean = true) : Resource<T>()
}
Repository:
override suspend fun getProductComments(productId: Int): Resource<List<Comment>> {
return try {
Resource.Loading<List<Comment>>()
delay(3000)
Resource.Success(apiService.getComments(productId))
} catch (t: Throwable) {
val mappedException = ExceptionMapper.map(t)
Resource.Error(message = t.message!!, errorType = mappedException.type)
}
}
ViewModel:
#HiltViewModel
class ProductDetailViewModel #Inject constructor(
state: SavedStateHandle,
private val productRepository: ProductRepository
) : ViewModel() {
private val passedProduct = state.get<Product>(EXTRA_KEY_DATA)
var productId = passedProduct?.id
var productState = mutableStateOf(ProductState())
private set
init {
getProductComments()
}
private fun getProductComments() {
viewModelScope.launch {
productId?.let { pId ->
when (val commentResult = productRepository.getProductComments(pId)) {
is Resource.Success -> {
commentResult.data?.let { comments ->
productState.value =
productState.value.copy(
comments = comments,
error = null,
isLoading = false
)
}
}
is Resource.Error -> {
productState.value = productState.value.copy(
isLoadFailed = true,
isLoading = false,
error = commentResult.message
)
}
is Resource.Loading -> {
productState.value = productState.value.copy(
isLoadFailed = false,
isLoading = true,
error = null
)
}
}
}
}
}
}
Your'e only checking this
is Resource.Loading -> {
...
}
when the repository returns, at this point its useless because when the call to getProductComments is done, it's already Resource.Success.
return try {
Resource.Loading<List<Comment>>() // you'll never get this value
delay(3000)
Resource.Success(apiService.getComments(productId))
So I'd suggest to update the ProductState before you call the repository
private fun getProductComments() {
productState.value = productState.value.copy(isLoading = true)
viewModelScope.launch {
...
...
or set isLoading to true as its initial state.
data class ProductState(
...
...
val isLoading : Boolean = true
...
)

How to retrieve data from Firestore with the MVVM pattern

I am creating an android application following the MVVM patron with the goal of retrieving data from a Firebase collection.
Before applying this pattern, I did proof of concept and I was able to retrieve data from the Firebase collection. But once I apply MVVM, I am not able to get the data from that collection, my screen does not show anything. I am not able to return the data from the repository to be painted on the screen.
This is my code:
Model:
data class PotatoesData(
val modifiedDate: String,
var potatoes: List<Potato>
)
data class Potato(
val type: String,
val site: String
)
State:
data class PotatoesState(
val isLoading: Boolean = false,
val potatoes: List<Potato> = emptyList(),
val error: String = ""
)
ModelView:
#HiltViewModel
class PotatoesViewModel #Inject constructor(
private val getPotatoesDataUseCase: GetPotatoesData
) : ViewModel() {
private val _state = mutableStateOf(PotatoesState())
val state: State<PotatoesState> = _state
init {
getPotatoes()
}
private fun getPotatoes() {
getPotatoesDataUseCase().onEach { result ->
when (result) {
is Resource.Success -> {
_state.value = PotatoesState(potatoes = result.data?.potatoes ?: emptyList())
}
is Resource.Error -> {
_state.value = PotatoesState(
error = result.message ?: "An unexpected error occurred"
)
}
is Resource.Loading -> {
_state.value = PotatoesState(isLoading = true)
}
}
}.launchIn(viewModelScope)
}
}
UseCase:
class GetPotatoesData #Inject constructor(
private val repository: PotatoRepository
) {
operator fun invoke(): Flow<Resource<PotatoesData>> = flow {
try {
emit(Resource.Loading())
val potatoes = repository.getPotatoesData()
emit(Resource.Success(potatoes))
} catch (e: IOException) {
emit(Resource.Error("Couldn't reach server. Check your internet connection."))
}
}
}
Repository implementation:
class PotatoRepositoryImpl : PotatoRepository {
override suspend fun getPotatoesData(): PotatoesData {
var potatoes = PotatoesData("TEST", emptyList())
FirestoreProvider.getLastPotatoes(
{ potatoesData ->
if (potatoesData != null) {
potatoes = potatoesData
}
},
{
potatoes
}
)
return potatoes
}
}
Firestore provider:
object FirestoreProvider {
private val incidentsRef = FirebaseFirestore.getInstance().collection(FirestoreCollection.POTATOES.key)
fun getLastPotatoes(
success: (potatoesData: PotatoesData?) -> Unit,
failure: () -> Unit
) {
val query: Query = orderBy(FirestoreField.CREATED_DATE, Query.Direction.DESCENDING).limit(1)
val querySnapshot: Task<QuerySnapshot> = query.get()
querySnapshot
.addOnSuccessListener {
if (!querySnapshot.result.isEmpty) {
val document = querySnapshot.result.documents[0]
val potatoesDataDB: PotatoesDataDto? = document.toObject(PotatoesDataDto::class.java)
potatoesDataDB?.let {
success(potatoesDataDB.toPotatoesData())
} ?: run {
success(null)
}
} else {
success(null)
}
}
.addOnFailureListener {
failure()
}
}
private fun orderBy(field: FirestoreField, direction: Query.Direction): Query {
return incidentsRef.orderBy(field.key, direction)
}
}
I am thankful for any kind of help! Thanks in advance!
I think the error is in the way of how you are handling Firestore callbacks. in FirestoreProvider: the callback will fire later than the function getLastPotatoes returns. Try to make that function suspend and use suspendCoroutine to wait for the callback and return it's result. It will look something like:
suspend fun getLastPotatoes() = suspendCoroutine <PotatoesData?> { continuation ->
val query: Query = orderBy(FirestoreField.CREATED_DATE, Query.Direction.DESCENDING).limit(1)
val querySnapshot: Task<QuerySnapshot> = query.get()
querySnapshot
.addOnSuccessListener {
if (!querySnapshot.result.isEmpty) {
val document = querySnapshot.result.documents[0]
val potatoesDataDB: PotatoesDataDto? = document.toObject(PotatoesDataDto::class.java)
potatoesDataDB?.let {
continuation.resume(potatoesDataDB.toPotatoesData())
} ?: run {
continuation.resume(null)
}
} else {
continuation.resume(null)
}
}
.addOnFailureListener {
continuation.resumeWithException(...)
}
}
suspendCoroutine suspends coroutine in which it executed until we decide to continue by calling appropriate methods - Continuation.resume....
In your PotatoRepositoryImpl:
override suspend fun getPotatoesData(): PotatoesData {
var potatoes = PotatoesData("TEST", emptyList())
try {
val potatoesData = FirestoreProvider.getLastPotatoes()
if (potatoesData != null) {
potatoes = potatoesData
}
} catch (e: Exception) {
// handle Exception
}
return potatoes
}

How to propagate the response of an async operation to the view using Jetpack Compose?

I have this sealed class:
sealed class Resource<out T> {
object Loading: Resource<Nothing>()
data class Success<out T>(val data: T): Resource<T>()
data class Failure(val message: String): Resource<Nothing>()
}
In the repository class I have this function that deletes an item from an API:
override suspend fun deleteItem(id: String) = flow {
try {
emit(Resource.Loading)
emit(Resource.Success(itemsRef.document(id).delete().await()))
} catch (e: Exception) {
emit(Resource.Failure(e.message))
}
}
The result of the delete operation is Void?. Now, in the ViewModel class I declare:
val state = mutableStateOf<Resource<Void?>>(Success(null))
And update it when the delete completes:
fun deleteItem(id: String) {
viewModelScope.launch {
repo.deleteItem(id).collect { response ->
state.value = response
}
}
}
I have created a Card and inside onClick I have added:
IconButton(
onClick = viewModel.deleteItem(id),
)
Which actually deletes that item form database correctly. But I cannot track the result of the operation. I tried using:
when(val res = viewModel.state.value) {
is Resource.Loading -> Log.d(TAG, "Loading")
is Resource.Success -> Log.d(TAG, "Success")
is Resource.Failure -> Log.d(TAG, "Failure")
}
But only the case Loading is triggered. No success/failure at all. What can be wrong here? As it really acts like a synchronous operation.
I've tested your approach without a repository, and compose part looks totally fine:
var i = 0
#Composable
fun TestScreen(viewModel: TestViewModel = viewModel()) {
val state by viewModel.state
Text(
when (val stateSmartCast = state) {
is Resource.Failure -> "Failure ${stateSmartCast.message}"
Resource.Loading -> "Loading"
is Resource.Success -> "Success ${stateSmartCast.data}"
}
)
Button(onClick = {
viewModel.deleteItem(++i)
}) {
}
}
class TestViewModel : ViewModel() {
val state = mutableStateOf<Resource<Int>>(Resource.Success(i))
fun deleteItem(id: Int) {
viewModelScope.launch {
deleteItemInternal(id).collect { response ->
state.value = response
}
}
}
suspend fun deleteItemInternal(id: Int) = flow {
try {
emit(Resource.Loading)
delay(1000)
if (id % 3 == 0) {
throw IllegalStateException("error on third")
}
emit(Resource.Success(id))
} catch (e: Exception) {
emit(Resource.Failure(e.message ?: e.toString()))
}
}
}
So the the problem looks like in this line itemsRef.document(id).delete().await()), or in your connection to the repository.
Try collecting in the composable function:
val state = viewModel.state.collectAsState()
Then you can do: when (val res = viewModel.state.value){...}.
However I am sceptical about the deleteItem in the repository returning a flow. Do you really need such thing? You can always map stuff in the viewModel.

NetworkOnMainThreadException when using rxandroid and mvvm design pattern

I have an issue with my code which is throwing NetworkOnMainThreadException. I am trying to connect to an Android app to Odoo using Android XML-RPC library.
Here is what I am doing.
class OdooServiceImpl : OdooService {
/* This is the only function doing network operation*/
override fun userAuthenticate(
host: String,
login: String,
password: String,
database: String
): Single<Int> {
val client = XMLRPCClient("$host/xmlrpc/2/common")
val result =
client.call("login", database, login, password)
return Single.just(result as Int)
}}
This class is called from a repository class.
The repository if called by the viewmodel class using rxandroid
class OdooViewModel(private val mainRepository: OdooRepository, private val context: Context) :
ViewModel() {
val host = "https://myodoo-domain.com"
private val user = MutableLiveData<OdooResource<Int>>()
private val compositeDisposable = CompositeDisposable()
init {
authUser()
}
private fun authUser(){
user.postValue(OdooResource.authConnecting(null))
compositeDisposable.add(
mainRepository.userAuthenticate(host,"mylogin","mypassword","mdb")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
if (it != null) {
user.postValue(OdooResource.authSuccess(it))
} else {
user.postValue(
OdooResource.authError(
null,
msg = "Something went wring while authenticating to $host"
)
)
}
}, {
server.postValue(
OdooResource.conError(
null,
msg = "Something went wring while authenticating to $host"
)
)
})
)
}
override fun onCleared() {
super.onCleared()
compositeDisposable.dispose()
}
fun getUser(): LiveData<OdooResource<Int>> {
return user
}
}
I have called this class from my activity as follow
class OdooActivity : AppCompatActivity() {
private lateinit var odooViewModel: OdooViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_odoo)
setupViewModel()
setupObserver()
}
private fun setupObserver() {
odooViewModel.getUser().observe(this, Observer {
Log.i("TAGGG", "Tests")
when (it.status) {
OdooStatus.AUTHENTICATION_SUCCESS -> {
progressBar.visibility = View.GONE
it.data?.let { server -> textView.setText(server.toString()) }
textView.visibility = View.VISIBLE
}
OdooStatus.AUTHENTICATION -> {
progressBar.visibility = View.VISIBLE
textView.visibility = View.GONE
}
OdooStatus.AUTHENTICATION_ERROR -> {
//Handle Error
progressBar.visibility = View.GONE
Toast.makeText(this, it.message, Toast.LENGTH_LONG).show()
}
else -> {
}
}
})
}
private fun setupViewModel() {
val viewModelFactory = OdooViewModelFactory(OdooApiHelper(OdooServiceImpl()), this)
odooViewModel = ViewModelProviders.of(this, viewModelFactory).get(OdooViewModel::class.java)
}
}
When I run the app this is a the line which is throwing the error
odooViewModel = ViewModelProviders.of(this, viewModelFactory).get(OdooViewModel::class.java)
What am I missing here??
The culprit is here:
val result = client.call("login", database, login, password)
return Single.just(result as Int)
The call to generate the result is executed, when setting up the Rx chain, which happens on the main thread. You have to make sure that the network-call is done when actually subscribing (on io()). One solution could be to return a Single.fromCallable:
return Single.fromCallable { client.call("login", database, login, password) as Int }

Exception handling of network errors retrofit

I was wondering what is the best way to handle network errors in retrofit requests when using coroutines.
The classic way is handling exception at highest level, when a request is made:
try {
// retrofit request
} catch(e: NetworkException) {
// show some error message
}
I find this solution wrong and it adds a lot of boilerplate code, instead I went with creating an interceptor that returns a error response:
class ErrorResponse : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
return try {
chain.proceed(request)
} catch (e: Exception) {
Snackbar.make(
view,
context.resources.getText(R.string.network_error),
Snackbar.LENGTH_LONG
).show()
Response.Builder()
.request(request)
.protocol(Protocol.HTTP_1_1)
.code(599)
.message(e.message!!)
.body(ResponseBody.create(null, e.message!!))
.build()
}
}
}
This solution is a little better, however I think that it can be improved.
So my question is: What is the correct way to handle the cases when user doesn't have internet connection, without a lot of boilerplate code (ideally with a global handler in case of connection errors) ?
Using Result to wrap my response
sealed class Result<out T : Any> {
data class Success<out T : Any>(val value: T) : Result<T>()
data class Failure(val errorHolder:ErrorHolder) : Result<Nothing>()}
ErrorHolder :
sealed class ErrorHolder(override val message):Throwable(message){
data class NetworkConnection(override val message: String) : ErrorHolder(message)
data class BadRequest(override val message: String) : ErrorHolder(message)
data class UnAuthorized(override val message: String) : ErrorHolder(message)
data class InternalServerError(override val message: String) :ErrorHolder(message)
data class ResourceNotFound(override val message: String) : ErrorHolder(message)
}
an extension to handle exeptions
suspend fun <T, R> Call<T>.awaitResult(map: (T) -> R): Result<R> = suspendCancellableCoroutine { continuation ->
try {
enqueue(object : Callback<T> {
override fun onFailure(call: Call<T>, throwable: Throwable) {
errorHappened(throwable)
}
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {
try {
continuation.resume(Result.Success(map(response.body()!!)))
} catch (throwable: Throwable) {
errorHappened(throwable)
}
} else {
errorHappened(HttpException(response))
}
}
private fun errorHappened(throwable: Throwable) {
continuation.resume(Result.Failure(asNetworkException(throwable)))
}
})
} catch (throwable: Throwable) {
continuation.resume(Result.Failure(asNetworkException(throwable)))
}
continuation.invokeOnCancellation {
cancel()
}}
And this how I make the api call:
suspend fun fetchUsers(): Result<List<User>> {
return service.getUsers().awaitResult { usersResponseDto ->
usersResponseDto.toListOfUsers()
}
}
UPDATE:
Let's say you have an error body like below:
{
"error" : {
"status" : 502,
"message" : "Bad gateway."
}
}
First we should create an data class to model response body
data class HttpErrorEntity(
#SerializedName("message") val errorMessage: String,
#SerializedName("status") val errorCode: Int
)
and here is asNetworkException implementation :
private fun asNetworkException(ex: Throwable): ErrorHolder {
return when (ex) {
is IOException -> {
ErrorHolder.NetworkConnection(
"No Internet Connection"
)
}
is HttpException -> extractHttpExceptions(ex)
else -> ErrorHolder.UnExpected("Something went wrong...")
}
}
private fun extractHttpExceptions(ex: HttpException): ErrorHolder {
val body = ex.response()?.errorBody()
val gson = GsonBuilder().create()
val responseBody= gson.fromJson(body.toString(), JsonObject::class.java)
val errorEntity = gson.fromJson(responseBody, HttpErrorEntity::class.java)
return when (errorEntity.errorCode) {
ErrorCodes.BAD_REQUEST.code ->
ErrorHolder.BadRequest(errorEntity.errorMessage)
ErrorCodes.INTERNAL_SERVER.code ->
ErrorHolder.InternalServerError(errorEntity.errorMessage)
ErrorCodes.UNAUTHORIZED.code ->
ErrorHolder.UnAuthorized(errorEntity.errorMessage)
ErrorCodes.NOT_FOUND.code ->
ErrorHolder.ResourceNotFound(errorEntity.errorMessage)
else ->
ErrorHolder.Unknown(errorEntity.errorMessage)
}
}
By implementing Interceptor, you are in right way. But by a little change, you can this sample class:
class NetworkConnectionInterceptor(val context: Context) : Interceptor {
#Suppress("DEPRECATION")
private val isConnected: Boolean
get() {
var result = false
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager?
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
cm?.run {
cm.getNetworkCapabilities(cm.activeNetwork)?.run {
result = when {
hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
else -> false
}
}
}
} else {
cm?.run {
cm.activeNetworkInfo?.run {
if (type == ConnectivityManager.TYPE_WIFI) {
result = true
} else if (type == ConnectivityManager.TYPE_MOBILE) {
result = true
}
}
}
}
return result
}
#Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
if (!isConnected) {
// Throwing your custom exception
// And handle it on onFailure
}
val builder = chain.request().newBuilder()
return chain.proceed(builder.build())
}
}
Then add it to your OkHttpClient.Builder():
.addInterceptor(NetworkConnectionInterceptor(context));
And in failure you can handle it in onFailure method like this:
override fun onFailure(call: Call<BaseModel>, t: Throwable) {
if (t is NoConnectivityException) {
// Handle it here :)
}
}

Categories

Resources