Custom response/error handler for retrofit 2.6 - android

I have the following class:
interface API
{
#GET("/data.json")
suspend fun fetchData() : MyResponse
companion object
{
private val BASE_URL = "http://10.0.2.2:8080/"
fun create(): MyAPI
{
val gson = GsonBuilder()
.setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
.create()
val retrofit = Retrofit.Builder()
.addConverterFactory( GsonConverterFactory.create( gson ) )
.baseUrl( BASE_URL )
.build()
return retrofit.create( MyAPI::class.java )
}
}
}
I'm calling it the following way:
val data = StadiumAPI.create().fetchData()
Log.d( "gotdata", data.toString() )
Everything works great, but now I want to handle errors, I'm trying to accomplish something like this:
Var response = StadiumAPI.create().fetchData()
when( response.state )
{
Success -> doSomethingWithTheData( response.data )
Error -> showError( response.error )
Processing -> showSpinner()
}
The main problem is that not only I would need to handle success/error (based on the HTTP status code and if GSON conversion was successful) but also handle exceptions (like network issues) and pass down them as Error to the response state as well as keep the automatic conversion with GSON without handling it manually.
I'm completely lost where to go from here. As far as I understood I need to create a custom data type in the retrofit response API which will "accept" the response and then I could manipulate its properties to produce the code structure above. Could you please point me in the right direction where should I go from here? Thanks!
----------- EDIT -------------------------------------------
I found out I can do what I'm trying the following way:
interface API
{
#GET("/data.json")
suspend fun fetchData() : ApiResponse<MyResponse>
....
}
And here is the ApiResponse:
sealed class ApiResponse<T> {
companion object {
fun <T> create(response: Response<T>): ApiResponse<T> {
Log.d( "networkdebug", "success: " + response.body().toString() )
return if(response.isSuccessful) {
Log.d( "networkdebug", "success: " + response.body().toString() )
val body = response.body()
// Empty body
if (body == null || response.code() == 204) {
ApiSuccessEmptyResponse()
} else {
ApiSuccessResponse(body)
}
} else {
val msg = response.errorBody()?.string()
Log.d( "networkdebug", "error: " + msg.toString() )
val errorMessage = if(msg.isNullOrEmpty()) {
response.message()
} else {
msg
}
ApiErrorResponse(errorMessage ?: "Unknown error")
}
}
}
}
class ApiSuccessResponse<T>(val data: T): ApiResponse<T>()
class ApiSuccessEmptyResponse<T>: ApiResponse<T>()
class ApiErrorResponse<T>(val errorMessage: String): ApiResponse<T>()
But for whatever reason the CompanionObject within the ApiResponse is not triggering at all, any hints on what I could be doing wrong? Thanks!

Related

#Get annotation does not accept any other arguments other than top-headlines (Kotlin)(NewsAPI)(Android Studio)

I am currently facing a problem with the endpoint argument in the #GET Annotation in Android Studio.
Here are the list of endpoints
NewsApi Endpoints
Defining the get function with the following endpoint v2/top-headlines works completely fine
interface APIDirectory{
#GET("v2/top-headlines")
suspend fun getArticles(
#Query("country")
countryCode: String = "Singapore",
#Query("page")
pageNumber: Int = 1,
#Query("sortBy")
sortBy:String = "publishedAt",
#Query("apiKey")
apiKey: String = API_KEY
):Response<ArticleList>
However, changing the endpoint to /v2/everything will result in an error
2022-12-03 02:47:11.887 10220-10220/com.example.thenewsapplication E/Tech: An error occured
This error is located in the Tech Fragment, the error occurs in the is Resource.Error portion of the code.
viewModel.searchNews.observe(viewLifecycleOwner, Observer{ response ->
Log.e("Tech", "in the technology fragment")
when (response){
is Resource.Success -> {
response.data?.let {newsResponse ->
//Submits the data to the recyclerview
newsAdapter.differ.submitList(newsResponse.articles.toList())
val totalPages = newsResponse.totalResults / Constants.QUERY_PAGE_SIZE + 2
isLastPage = viewModel.searchNewsPage == totalPages
}
}
is Resource.Error -> {
response.message?.let {
Log.e("Tech","An error occured $it")
}
}
is Resource.Loading -> {
...}
I concluded that the error must be due to the response argument in the when statement, which refers to the searchNews variable in the viewmodel
val searchNews: MutableLiveData<Resource<ArticleList>> = MutableLiveData()
fun getsearchNews(countryCode: String) = viewModelScope.launch {
Log.e("viewmodel", "in the viewmodel getsearchNews")
searchNews.postValue(Resource.Loading())
Log.e("viewmodel", "getting data")
val response = newsRepository.getSearchNews(countryCode, breakingNewsPage)
searchNews.postValue(handleSearchNewsResponse(response))
}
private fun handleSearchNewsResponse(response: Response<ArticleList>): Resource<ArticleList> {
if (response.isSuccessful) {
response.body()?.let { resultResponse ->
searchNewsPage++
if (searchNewsResponse == null) {
searchNewsResponse = resultResponse
} else {
val olArticles = searchNewsResponse?.articles
val newArticles = resultResponse.articles
olArticles?.addAll(newArticles)
}
return Resource.Success(searchNewsResponse ?: resultResponse)
}
}
return Resource.Error(response.message())
}
From the handleSearchNewsResponse above, the Response from the ArticleList is a failure, the ArticleList class is just a kotlin class that represents the Response from the WebService
data class ArticleList(
val articles: MutableList<Article>,
val status: String,
val totalResults: Int
)

How to store the return value of suspended function to a variable?

I'm trying to understand Kotlin couroutine. So here's my code (based on this tutorial). To keep the code relatively simple, I deliberately avoid MVVM, LiveData, etc. Just Kotlin couroutine and Retrofit.
Consider this login process.
ApiInterface.kt
interface ApiInterface {
// Login
#POST("/user/validate")
suspend fun login(#Body requestBody: RequestBody): Response<ResponseBody>
}
ApiUtil.kt
class ApiUtil {
companion object {
var API_BASE_URL = "https://localhost:8100/testApi"
fun getInterceptor() : OkHttpClient {
val logging = HttpLoggingInterceptor()
logging.level = HttpLoggingInterceptor.Level.BODY
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(logging)
.build()
return okHttpClient
}
fun createService() : ApiInterface {
val retrofit = Retrofit.Builder()
.client(getInterceptor())
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(OJIRE_BASE_URL)
.build()
return retrofit.create(ApiInterface::class.java)
}
}
fun login(userParam: UserParam): String {
val gson = Gson()
val json = gson.toJson(userParam)
var resp = ""
val requestBody = json.toString().toRequestBody("application/json".toMediaTypeOrNull())
CoroutineScope(Dispatchers.IO).launch {
val response = createService().login(requestBody)
withContext(Dispatchers.Main){
if (response.isSuccessful){
val gson = GsonBuilder().setPrettyPrinting().create()
val prettyJson = gson.toJson(
JsonParser.parseString(
response.body()
?.string()
)
)
resp = prettyJson
Log.d("Pretty Printed JSON :", prettyJson)
}
else {
Log.e("RETROFIT_ERROR", response.code().toString())
}
}
}
return resp
}
}
LoginActivity.kt
class LoginActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
edtUsername = findViewById(R.id.edtUsername)
edtPassword = findViewById(R.id.edtPassword)
btnLogin = findViewById(R.id.btnLogin)
btnLogin.setOnClickListener {
val api = ApiUtil()
val userParam = UserParam(edtMobileNo.text.toString(), edtPassword.text.toString())
val response = JSONObject(api.login(userParam))
var msg = ""
if (response.getString("message").equals("OK")){
msg = "Login OK"
}
else {
msg = "Login failed"
}
Toast.makeText(applicationContext, msg, Toast.LENGTH_SHORT).show()
}
}
}
When debugging the login activity, the API response is captured properly on prettyJson
The problem is resp is still empty. Guess that's how async process work. What I want is to wait until the API call is completed, then the result can be nicely passed to resp as the return value of login(). How to do that?
Well, you got several things wrong here. We'll try to fix them all.
First, the main problem you described is that you need to acquire resp in login() synchronously. You got this problem only because you first launched an asynchronous operation there. Solution? Don't do that, get the response synchronously by removing launch(). I guess withContext() is also not required as we don't do anything that requires the main thread. After removing them the code becomes much simpler and fully synchronous.
Last thing that we need to do with login() is to make it suspendable. It needs to wait for the request to finish, so it is a suspend function. The resulting login() should be similar to:
suspend fun login(userParam: UserParam): String {
val gson = Gson()
val json = gson.toJson(userParam)
val requestBody = json.toString().toRequestBody("application/json".toMediaTypeOrNull())
val response = createService().login(requestBody)
return if (response.isSuccessful){
val gson = GsonBuilder().setPrettyPrinting().create()
gson.toJson(
JsonParser.parseString(
response.body()
?.string()
)
)
}
else {
Log.e("RETROFIT_ERROR", response.code().toString())
// We need to do something here
}
}
Now, as we converted login() to suspendable, we can't invoke it from the listener directly. Here we really need to launch asynchronous operation, but we won't use CoroutineScope() as you did in your example, because it leaked background tasks and memory. We will use lifecycleScope like this:
btnLogin.setOnClickListener {
val api = ApiUtil()
val userParam = UserParam(edtMobileNo.text.toString(), edtPassword.text.toString())
lifecycleScope.launch {
val response = JSONObject(api.login(userParam))
var msg = ""
if (response.getString("message").equals("OK")){
msg = "Login OK"
}
else {
msg = "Login failed"
}
withContext(Dispatchers.Main) {
Toast.makeText(applicationContext, msg, Toast.LENGTH_SHORT).show()
}
}
}
Above code may not be fully functional. It is hard to provide working examples without all required data structures, etc. But I hope you get the point.
Also, there are several other things in your code that could be improved, but I didn't touch them to not confuse you.

The problem is with retrofit when running on a physical device

I have a problem: I'm writing a weather app, using retrofit 2.0. When I run the app on the emulator, everything works fine(API 24, 28, 29). But today I launched my app on a physical device (Galaxy A21s, version android 10) and the request to the server is not working. The request works onResponse() but it comes with response.body () = = null and response.is Successful == null. But everything works in the emulator!
Can you tell us what the problem is and how to solve it?
class DataProcessing {
private val retrofitImpl: RetrofitImpl = RetrofitImpl()
private val mainActivity = MainActivity()
internal fun sendRequest(townName:String, instance : DataProcessingCallback){
retrofitImpl.getRequest().showWeather(townName).enqueue(object : Callback<DateWeather> {
override fun onResponse(call: retrofit2.Call<DateWeather>, response: Response<DateWeather>) {
if (response.isSuccessful && response.body() != null) {
processingData(response.body(), null, instance)
} else
processingData(null, Throwable("ответ не получен"), instance)
}
override fun onFailure(call: Call<DateWeather>, t: Throwable) {
Log.d("Main", "onFailure")
processingData(null, t, instance)
}
})
}
private fun processingData(dateWeather:DateWeather?, error: Throwable?, instance : DataProcessingCallback){
if (dateWeather == null || error != null) {
Log.d("Egor", "error: ${error!!.message.toString()}")
instance.showToastText("Произошла ошибка \n Возможно вы неправильно ввели название населенного пункта")
} else {
if (dateWeather == null) Log.d("Main", "Loose")
else {
val string = dateWeather.weather.get(0).toString()
val size = string.length - 1
instance.onSuccessfulDataProcessed(string.subSequence(13, size).toString(), dateWeather.main.temp!!.toInt())
}
}
}
}
interface ShowWeather{
#GET("weather?&appid=(TOKEN)&units=metric")// there is a token here, I just deleted it when publishing, everything is fine with it
fun showWeather(#Query("q") town: String): Call<DateWeather>
}
class RetrofitImpl{
fun getRequest() : ShowWeather{
val retrofitBuilder = Retrofit.Builder()
.baseUrl("http://api.openweathermap.org/data/2.5/")
.addConverterFactory(GsonConverterFactory.create())
.build()
return retrofitBuilder.create(ShowWeather::class.java)
}
}
data class DateWeather(
val main: Main,
val weather : List<Weather>
)
data class Main(
val temp : Double?
)
data class Weather(
val main: String
)
At a base URL, you are trying with http unsecured connection. Can you check with "https://" instead of "http://"
val retrofitBuilder = Retrofit.Builder()
.baseUrl("https://api.openweathermap.org/data/2.5/")
.addConverterFactory(GsonConverterFactory.create())
.build()
The error turned out to be elementary: on a physical device, I used a hint (T9) that returned the city. After the name of the city there was a gap and this was the error. trim () solved my problem.
#Kishore A, thank you so much for your help!
townName = editText.getText().trim().toString()

Android generic Gson.fromJson() conversion with coroutine and flow

I am making generic classes for hitting otp api.anybody can use otp section just have to pass request ,Response class and url and all will be done by this otp section.
Please note : this response class can be of different type (for eg: MobileOtpResponse,EmailOtpResponse)
below is the generic OtpClient which takes any request type and returns particular passed ResponseType (for example : Request class passed is OtpRequest ,ResponseType class passed is OtpResponse)
interface OtpClient {
#POST
suspend fun <Request : Any, ResponseType> sendOtp(#Url url: String,
#Body request:#JvmSuppressWildcards Any): #JvmSuppressWildcards ResponseType
}
OtpRequest
data class OtpRequest(#SerializedName("mobile_number") val mobileNumber: String,#SerializedName("app_version") val appVersion: String)
OtpResponse
data class OtpResponse(#SerializedName("status") val status: String = "",
#SerializedName("response") val response: OtpData? = null)
data class OtpData(
#SerializedName("otp_status") val otpStatus: Boolean = false,
#SerializedName("message") val message: String = "",
#SerializedName("error") val error: Int? = null,
#SerializedName("otp_length") val otpLength: Int? = null,
#SerializedName("retry_left") val retryLeft: Int? = null,)
Now i create Repo to call this api this simply use flow and when the data fetch it emits the data
class OtpRepoImpl<out Client : OtpClient>(val client: Client) :OtpRepo {
override fun <Request:Any, ResponseType> sentOtpApi(url: String, request: Request): Flow<ResponseType> {
return flow<ResponseType> {
// exectute API call and map to UI object
val otpResponse = client.sendOtp<Request, ResponseType>(url,request)
emit(otpResponse)
}.flowOn(Dispatchers.IO) // Use the IO thread for this Flow
}
}
this repo is used in viewmodel class
#ExperimentalCoroutinesApi
fun <A : Class<ResponseType>, Request : Any, ResponseType : Any> sendOtp(a: Class<ResponseType>, request: Request, response: ResponseType, url: String) {
viewModelScope.launch {
repo.sentOtpApi<Request, ResponseType>(url, request = request)
.onStart { _uiState.value = OtpState.Loading(true) }
.catch { cause ->
_uiState.value = OtpState.Loading(false)
getResponseFromError<Class<ResponseType>,ResponseType>(cause, response) {
// emit(it)
}
}
.collect {
_uiState.value = OtpState.Loading(false)
_uiState.value = OtpState.Success(it)
}
}
}
as you can see above this sendOtp method is called from the view class and inside this method we use repo.sentOtpApi and pass generic request response type.I get data in catch block coz api is send error otp data in 400 HttpException so i created another method getResponseFromError to get error response it should parse the errorBody response and call this lambda block.
private suspend fun <A : Class<*>, ResponseType : Any> getResponseFromError( cause: Throwable, rp: ResponseType, block: suspend (ResponseType) -> Unit) {
if (cause is HttpException) {
val response = cause.response()
if (response?.code() == 400) {
println("fetching error Response")
val errorResponse = response.errorBody()?.charStream()
val turnsType = object : TypeToken<ResponseType>() {}.type
val finalErrorResponse = Gson().fromJson<ResponseType>(errorResponse, turnsType)
block(finalErrorResponse)
} else {
println("someOther exception")
}
} else
_uiState.value = OtpState.Error(cause)
}
so here i am facing the problem inside above method
val turnsType = object : TypeToken<ResponseType>() {}.type
val finalErrorResponse = Gson().fromJson<ResponseType>(errorResponse, turnsType)
block(finalErrorResponse)
This finalErrorResponse is returning LinkedTreeMap instead of ResponseType (in this case its OtpResponse)
i have also tried using Class<*> type like this
val turnsType = object : TypeToken<A>() {}.type
val finalErrorResponse = Gson().fromJson<A>(errorResponse, turnsType)
but its not working.
calling of this sentOtp viewmodel func is like
var classType = OtpResponse::class.java
otpViewModel.sendOtp(a = classType, request = otpRequest, response = OtpResponse() , url =
"http://preprod-api.nykaa.com/user/otp/v2/send-wallet-otp")
[![value in finalErroResponse][1]][1]
[1]: https://i.stack.imgur.com/Holui.png
required: finalErroResponse should be of OtpResponse type because that was passed in sentOtp func
Please help :)

What is the best practice for parsing retrofit response on android with coroutines

I am trying to figure out what is the best practice for handling a retrofit response.
I provide the retrofit singlton like this:
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(AuthInterceptor())
.addInterceptor(httpLoggingInterceptor)
.build()
val retrofit = Retrofit.Builder()
.client(okHttpClient)
.baseUrl(BuildConfig.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(CoroutinesResponseCallAdapterFactory())
.build()
val service = retrofit.create(ShowsService::class.java)
The service interface is this:
interface ShowsService {
#GET("popular")
suspend fun fetchPopularShows(): Response<PopularShows>
}
I get a list of shows from API and parse it in a repository like this:
override suspend fun getShows(): Result<List<Show>?> {
val shows = service.fetchPopularShows()
val body = shows.body()
val errorBody = shows.errorBody()
return when {
body != null -> {
Result.Success(body.shows)
}
errorBody != null -> {
Result.Error(Exception(errorBody.string()))
}
else -> {
Result.Error(Exception("Unknown error: ${shows.raw().message}"))
}
}
}
However, this feels very non-kotlin and also would probably result in code duplication eventually, can anyone point me to a sample where this is implemented in the best practice?
In principle, you could create an unwrapResponse() generic function that takes a Response<T> and returns a Result<T?> and incorporates your algorithm. By eyeball, something like this:
suspend fun <T> unwrapResponse(response: Response<T>): Result<T> {
val body = response.body()
val errorBody = response.errorBody()
return when {
body != null -> {
Result.Success(body)
}
errorBody != null -> {
Result.Error(Exception(errorBody.string()))
}
else -> {
Result.Error(Exception("Unknown error: ${response.raw().message}"))
}
}
}
You could then call unwrapResponse(service.fetchPopularShows()) to get a Result<PopularShows>.
If you really wanted to allow unwrapResponse() to return a Result<List<Show>?>, you would wind up with something like:
suspend fun <T, R> unwrapResponse(response: Response<T>, unpacker: (T) -> R?): Result<R?> {
val body = response.body()
val errorBody = response.errorBody()
return when {
body != null -> {
Result.Success(unpacker(body))
}
errorBody != null -> {
Result.Error(Exception(errorBody.string()))
}
else -> {
Result.Error(Exception("Unknown error: ${response.raw().message}"))
}
}
}
unwrapResponse(service.fetchPopularShows()) { it.shows } would then give your Result<List<Show>?>.
Again, this is all by eyeball — adjustments are likely to be needed here.

Categories

Resources