When I enter the city name correctly everything goes fine but when the user enters the wrong city name it causes this error
{
"error": {
"code": 1006,
"message": "No matching locations found."}
}
How can I handle this error?
Api
interface Api{
#GET("forecast.json")
suspend fun getCurrentTemp(#Query("key")key : String, #Query("q")q: String,
#Query("days")days : Int): Response<Weatherapi>
companion object {
operator fun invoke(
):Api{
return Retrofit.Builder().baseUrl("https://api.weatherapi.com/v1/")
.addConverterFactory(GsonConverterFactory.create())
.build().create(Api::class.java)
}
}
}
Repository:
abstract class repositoryApi {
suspend fun <T : Any> CustomResponse(work: () ->Response <T>): T {
val response: Response<T> = work.invoke()
if (response.isSuccessful)
return response.body()!!
throw Exception(response.message())
}
}
handelRequst:
object handelRequst: repositoryApi() {
suspend fun <T:Any> Requst (response: Response<T>) = CustomResponse { response } }
handelCoroutins:
object handelCoroutins {
fun <T:Any> ThreadMain(work:suspend (() -> T) ,callback : ((T) -> Unit),ErrorMessage :
((String) -> Unit))=
CoroutineScope(Dispatchers.Main).launch {
try{
val data :T = CoroutineScope(Dispatchers.IO).async rt#{
return#rt work()
}.await()
callback(data)
}catch(e : IOException){
ErrorMessage.invoke("Error C")
}
}
}
viewModel:
class viewModelapi: ViewModel() {
val LivedataErrorhandel = MutableLiveData<String>()
var weather = MutableLiveData<Weatherapi>()
lateinit var job: Job
fun Gethome(key :String , q :String ,days :Int) {
try {
job = handelCoroutins.ThreadMain(
{
handelRequst.Requst(Api.invoke().getCurrentTemp(key ,q ,days))
},
{
weather.value = it
}, {
LivedataErrorhandel.value = it
}
)
} catch (e: IOException) {
LivedataErrorhandel.value = "Error C"
}
}
}
main Activity :
viewmodel.weather.observe(requireActivity(), Observer{
textViewtemp.text = it.current.temp_c.toString()
}
I'm not giving the full answer I just give you an idea of how you can handle this. Here's some code you might look at carefully I hope you can take this your way.
if (response.isSuccessful) {
return response.body()!!
} else {
//this is a json object that you should return for handle error
var error:JSONObject? = null
try {
//heres I convert error response to json object
error = JSONObject(response.errorBody()!!.charStream().readText())
//you may know how can you get this exception on your api implementation area
throw CustomException(error)
} catch (e: JSONException) {
throw Exception("Something is wrong !! ")
}
}
CustomException class
class CustomException(error:JsonObject):Exception()
heres how you should implement
try {
job = handelCoroutins.ThreadMain(
{
handelRequst.Requst(Api.invoke().getCurrentTemp(key ,q ,days))
},
{
weather.value = it
}, {
LivedataErrorhandel.value = it
}
)
} catch (e: IOException) {
LivedataErrorhandel.value = "Error C"
}catch(error:CustomException){
//heres you got the json object }
Related
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
}
I understand how to handle errors when not using coroutines:
#GET("user/{user}")
fun getHomeData(#Path("user") user: String?): Call<HomeDataBody>
fun getHomeData(id:String, callback: (Boolean, String?) -> Unit)
{
val call = service.getHomeData(id)
call.enqueue( object : Callback<HomeDataBody> {
override fun onResponse(call: Call<HomeDataBody>, response: Response<HomeDataBody>)
{
if (response.isSuccessful)
{
dataMgr.homeData = response.body()!!.user
callback(true, null)
}
else
{
callback(false, response.message())
}
}
override fun onFailure(call: Call<HomeDataBody>, t: Throwable)
{
callback(false, t.message)
}
})
}
But I cannot for the life of me figure out how to do this with coroutines, this is what I have for a coroutine that does not return errors:
#GET("user/{user}")
suspend fun getHomeDataCoroutine(#Path("user") user: String?): HomeData
suspend fun getHomeDataCoroutine(id:String) : Pair<Boolean, String>
{
val data = service.getHomeDataCoroutine(id)
if(data != null)
{
dataMgr.homeData = data
}
else
{
return Pair(false, "how do i get the error message??")
}
}
I also attempted this, but when I try to call service.getHomeDataCoroutine I get this error:
java.lang.IllegalArgumentException: Unable to create call adapter for class java.lang.Object
for method RiseServiceRetro.getHomeDataCoroutine
#GET("user/{user}")
suspend fun getHomeDataCoroutine(#Path("user") user: String?): Deferred<HomeDataBody>?
sealed class Result<out T : Any>
class Success<out T : Any>(val data: T) : Result<T>()
class Error(val exception: Throwable, val message: String = exception.localizedMessage) : Result<Nothing>()
suspend fun getHomeDataCoroutine(id:String): Result<HomeDataBody>
{
try {
val response = service.getHomeDataCoroutine(id)!!.await()
return Success(response)
} catch (e: Exception) {
return Error(e)
}
}
To handle errors when calling suspend function of Retrofit service wrap it in try-catch block:
#GET("user/{user}")
suspend fun getHomeDataCoroutine(#Path("user") user: String?): HomeDataBody
suspend fun getHomeDataCoroutine(id:String): Pair<Boolean, String> {
return try {
val data = service.getHomeDataCoroutine(id)
dataMgr.homeData = data
Pair(true, "")
} catch(e: Throwable) {
Pair(false, e.message ?: "error")
}
}
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 :)
}
}
I use Moshi and I need to solve my problem with a buggy backend. Sometimes, when I request a list of objects, some of them don't contain mandatory fields. Of course, I can catch and process JsonDataException, but I want to skip these objects. How can I do it with Moshi?
Update
I have a couple of models for my task
#JsonClass(generateAdapter = true)
data class User(
val name: String,
val age: Int?
)
#JsonClass(generateAdapter = true)
data class UserList(val list: List<User>)
and buggy JSON
{
"list": [
{
"name": "John",
"age": 20
},
{
"age": 18
},
{
"name": "Jane",
"age": 21
}
]
}
as you can see, the second object has no mandatory name field and I want to skip it via Moshi adapter.
There's a gotcha in the solution that only catches and ignores after failure. If your element adapter stopped reading after an error, the reader might be in the middle of reading a nested object, for example, and then the next hasNext call will be called in the wrong place.
As Jesse mentioned, you can peek and skip the entire value.
class SkipBadElementsListAdapter(private val elementAdapter: JsonAdapter<Any?>) :
JsonAdapter<List<Any?>>() {
object Factory : JsonAdapter.Factory {
override fun create(type: Type, annotations: Set<Annotation>, moshi: Moshi): JsonAdapter<*>? {
if (annotations.isNotEmpty() || Types.getRawType(type) != List::class.java) {
return null
}
val elementType = Types.collectionElementType(type, List::class.java)
val elementAdapter = moshi.adapter<Any?>(elementType)
return SkipBadElementsListAdapter(elementAdapter)
}
}
override fun fromJson(reader: JsonReader): List<Any?>? {
val result = mutableListOf<Any?>()
reader.beginArray()
while (reader.hasNext()) {
try {
val peeked = reader.peekJson()
result += elementAdapter.fromJson(peeked)
} catch (ignored: JsonDataException) {
}
reader.skipValue()
}
reader.endArray()
return result
}
override fun toJson(writer: JsonWriter, value: List<Any?>?) {
if (value == null) {
throw NullPointerException("value was null! Wrap in .nullSafe() to write nullable values.")
}
writer.beginArray()
for (i in value.indices) {
elementAdapter.toJson(writer, value[i])
}
writer.endArray()
}
}
It seems I've found the answer
class SkipBadListObjectsAdapterFactory : JsonAdapter.Factory {
override fun create(type: Type, annotations: MutableSet<out Annotation>, moshi: Moshi): JsonAdapter<*>? {
return if (annotations.isEmpty() && Types.getRawType(type) == List::class.java) {
val elementType = Types.collectionElementType(type, List::class.java)
val elementAdapter = moshi.adapter<Any>(elementType)
SkipBadListObjectsAdapter(elementAdapter)
} else {
null
}
}
private class SkipBadListObjectsAdapter<T : Any>(private val elementAdapter: JsonAdapter<T>) :
JsonAdapter<List<T>>() {
override fun fromJson(reader: JsonReader): List<T>? {
val goodObjectsList = mutableListOf<T>()
reader.beginArray()
while (reader.hasNext()) {
try {
elementAdapter.fromJson(reader)?.let(goodObjectsList::add)
} catch (e: JsonDataException) {
// Skip bad element ;)
}
}
reader.endArray()
return goodObjectsList
}
override fun toJson(writer: JsonWriter, value: List<T>?) {
throw UnsupportedOperationException("SkipBadListObjectsAdapter is only used to deserialize objects")
}
}
}
Thank you "guys from the other topics" =)
You can find a working solution here:
https://github.com/square/moshi/issues/1288
Happy fixing :)
I am new to RX Java. While implementing Rx java with Retrofit i found i am getting throw-able object in my doOnError(){}
But what i want my doOnError() of RX Java should return ErrorBase() -> that is my custom class. instead of throwable.
it will help me to handle error at central. i will pass my throw-able object to my ErrorBase class where i have handled custom messages.
Below is doOnError(). Where i want to return ErrorBase object
apiInterface.getLoginDetails(auth)
.doOnNext {
//LoginResponse
}
doOnError{
return ErrorBase(throwable)
}
Code of other classes.
Api Interface class
interface ApiInterface {
#POST("login")
fun getLoginDetails(#Header(Constants.AUTHORIZATION) auth: String): Observable<LoginResponseModel>
}
LoginRepository
class LoginRepository #Inject constructor(private val apiInterface: ApiInterface,
val utils: Utils) {
fun getLoginDetails(auth: String): Observable<LoginResponseModel> {
return apiInterface.getLoginDetails(auth)
.doOnNext {
}
.doOnError {
//Right now having throw-able object
}
}
}
ErrorBase
class ErrorBase(private val throwable: Throwable) {
private var message: String?
private var statusCode: Int
init {
statusCode = getStatusCode()
message = getMessage()
}
private fun getStatusCode(): Int {
if (throwable is HttpException) {
val exception = throwable
return exception.code()
}
return -1
}
private fun getMessage() =
when (throwable) {
is IOException -> "Something Went Wrong"
is UnknownHostException -> "No internet connectivity"
is SocketTimeoutException -> "Slow Internet connectivity"
else -> throwable.message
}
}
LoginvViewModel
class LoginViewModel #Inject constructor(
private val loginRepository: LoginRepository) : ViewModel() {
private val TAG = this.javaClass.name
private var loginResult: MutableLiveData<LoginResponseModel> = MutableLiveData()
private var loginError: MutableLiveData<String> = MutableLiveData()
private var loginLoader: MutableLiveData<Boolean> = MutableLiveData()
private lateinit var disposableObserver: DisposableObserver<LoginResponseModel>
fun loginResult(): LiveData<LoginResponseModel> {
return loginResult
}
fun loginError(): LiveData<String> {
return loginError
}
fun loginLoader(): LiveData<Boolean> {
return loginLoader
}
private fun getLoginData(auth: String) {
loginLoader.postValue(true)
initLoginObserver()
loginRepository.getLoginDetails(auth)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.debounce(400, MILLISECONDS)
.subscribe(disposableObserver)
}
private fun initLoginObserver() {
disposableObserver = object : DisposableObserver<LoginResponseModel>() {
override fun onComplete() {
}
override fun onNext(loginDetails: LoginResponseModel) {
loginResult.postValue(loginDetails)
loginLoader.postValue(false)
}
override fun onError(e: Throwable) {
loginError.postValue(e.message)
loginLoader.postValue(false)
}
}
}
fun disposeElements() {
if (null != disposableObserver && !disposableObserver.isDisposed) disposableObserver.dispose()
}
fun loginClicked() {
getLoginData("auth")
}}
Firstly, doOnError isn't aimed to transform/return some data, but helps to handle side-effects like logging.
Second thing, ErrorBase doesn't fit well together with LoginResponseModel cause they don't have any common parent.
Thus, I suggest you following solution:
Create one base class for your response:
sealed class LoginResponse {
class Result( ..your data here.. ) : LoginResponse()
class Error( ... ) : LoginResponse()
}
Make function return LoginResponse and do following changes:
fun getLoginDetails(auth: String): Observable<LoginResponse> {
return apiInterface.getLoginDetails(auth)
.map { data -> LoginResponse.Result(data) }
.onErrorReturn { throwable -> LoginResponse.Error(throwable) }
}
Now both results have one common parent and you can use getLoginDetails in the following way:
fun doRequest() {
loginRepository.getLoginDetails(auth)
.subscribe { result ->
when (result) {
is LoginResponse.Result -> //do something with result
is LoginResponse.Error -> //do something with error
}
}
}
Some explanation.
onErrorReturn does exactly what you need - returns your custom value in case if error occurs
If you don't add LoginResponse you have to make Observable<Any> which is loosely typed and doesn't really well describes your interface.
Making LoginResponse sealed allows to check only 2 cases whether emitted data is Result or Error. Otherwise Kotlin compiler forces you to add additional else branch
Update In case if you need to do same thing in multiple places you can go with this:
sealed class Response<T> {
data class Result<T>(val result: T) : Response<T>()
data class Error<T>(val throwable: Throwable) : Response<T>()
}
fun getLoginDetails(auth: String): Observable<Response<LoginResponseModel>> {
return apiInterface.getLoginDetails(auth)
.map<Response<LoginResponseModel>> { data -> Response.Result(data) }
.onErrorReturn { throwable -> LoginResponse.Error(throwable) }
}
..and somewhere in your code..
fun handleResponse(response: Response<LoginData>) {
when (response) {
is Response.Result -> response.result
is Response.Error -> response.throwable
}
}