Moshi JsonAdapter to handle Observable<Bitmap> in Retrofit - android

Currently, I'm decoding Bitmap the following way:
#GET("api/users/get_avatar/{userId}/default.png")
fun getAvatar(#Header("ApiToken") apiToken: String, #Path("userId") userId: String): Observable<ResponseBody>
and decoding it in ViewModel
val avatar = it()?.let { body ->
val stream = body.byteStream()
BitmapFactory.decodeStream(stream)
}
However, I would like to use for that more elegant Moshi JsonAdapter.
My call looks like:
#GET("api/users/get_avatar/{userId}/default.png")
fun getAvatar(#Header("ApiToken") apiToken: String, #Path("userId") userId: String): Observable<Bitmap>
I'm adding adapter:
return Moshi.Builder()
.add(BitmapAdapter())
However, most probably my adapter is wrong:
private class BitmapAdapter {
#ToJson
fun toJson(value: Bitmap): String {
return value.encodeBase64()
}
#FromJson
fun fromJson(value: String): Bitmap {
return value.decodeBase64()
}
}
How it should look like?

Moshi is meant for parsing JSON, not directly decoding images. If you want to get a Bitmap from a Retrofit client, you'd want a Converter.Factory to supply directly to Retrofit.
Example:
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import okhttp3.ResponseBody
import retrofit2.Converter
import retrofit2.Retrofit
import java.lang.reflect.Type
class BitmapConverterFactory : Converter.Factory() {
override fun responseBodyConverter(type: Type, annotations: Array<Annotation>, retrofit: Retrofit): Converter<ResponseBody, *>? {
return if (type == Bitmap::class.java) {
Converter<ResponseBody, Bitmap> {
value -> BitmapFactory.decodeStream(value.byteStream())
}
} else {
null
}
}
}
And supply it wherever you instantiate your Retrofit instance:
Retrofit.Builder()
.baseUrl("https://myapi.com")
.addConverterFactory(BitmapConverterFactory())
.addConverterFactory(MoshiConverterFactory.create())
.build()
Edit: I initially made a mistake in BitmapCoverterFactory. The comparison of type was initially against Bitmap::javaClass, it should be Bitmap::class.java.

Related

mocking actual API call during unittesting retrofit interface

learning about retrofit but couldn't write the tests for it. I came from jest background and struggling to test two things:
that the call was making to a specific end point and its status.
import okhttp3.ResponseBody
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Query
interface WeatherAPI {
#GET("current.json")
suspend fun getCurrentWeatherData(#Query("key") apiKey: String, #Query("q") cityName: String, #Query("qui") quiValue: String): Response<ResponseBody>
companion object {
private const val BASE_URL = "http://api.weatherapi.com/v1/"
val instance: WeatherAPI by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(WeatherAPI::class.java)
}
}
}
Test file:
class WeatherAPITest {
#Test
fun WeatherAPI_getCurrentWeather_apiKey_city(){
runBlocking {
val res = WeatherAPI.instance.getCurrentWeatherData("123", "London", "no")
assertThat(res.code()).isEqualTo(200)
}
}
}
it fails because there is no token. How can i mock the actual api call to return say 200 on the test and confirm something like this:
assertThat(url).isEqualTo('http:....?key=123&q=London')
assertThat(responseCode).isEqualTo(200)

Retrofit error :- Expected BEGIN_ARRAY but was BEGIN_OBJECT at path$

I am trying to retrieve several rows from an API using Retrofit and Moshi, but am facing this error:
Retrofit error:- Expected BEGIN_ARRAY but was BEGIN_OBJECT at path$
The API endpoint I am requesting the data from is:
https://thecodecafe.in/gogrocer-ver2.0/api/top_selling
This is the setup code for Retrofit and Moshi that I am using to request the data from the API:
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.GET
private const val BASE_URL = "https://thecodecafe.in/gogrocer-ver2.0/api/"
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl(BASE_URL)
.build()
interface GroceryApiServices {
#GET("top_selling")
fun getProperties():
Call<List<GroceryProperty>>
}
object GroceryApi {
val retrofitServices: GroceryApiServices by lazy { retrofit.create(GroceryApiServices::class.java)}
}
This is the logic of my view model class, showing how I want to retrieve the data:
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.kotlin_developer.grocerysell.network.GroceryApi
import com.kotlin_developer.grocerysell.network.GroceryProperty
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class OverviewViewModel: ViewModel() {
private val _response = MutableLiveData<String>()
val response: LiveData<String>
get() = _response
init {
getGroceryProperties()
}
private fun getGroceryProperties(){
GroceryApi.retrofitServices.getProperties().enqueue(object : Callback<List<GroceryProperty>>{
override fun onFailure(call: Call<List<GroceryProperty>>, t: Throwable) {
_response.value = t.message
}
override fun onResponse(
call: Call<List<GroceryProperty>>,
response: Response<List<GroceryProperty>>
) {
_response.value="Success ${response.body()?.size} Grocery Property arrived"
}
})
}
override fun onCleared() {
super.onCleared()
}
}
This is how I want to retrieve data in my view model class
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.kotlin_developer.grocerysell.network.GroceryApi
import com.kotlin_developer.grocerysell.network.GroceryProperty
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class OverviewViewModel: ViewModel() {
private val _response = MutableLiveData<String>()
val response: LiveData<String>
get() = _response
init {
getGroceryProperties()
}
private fun getGroceryProperties(){
GroceryApi.retrofitServices.getProperties().enqueue(object : Callback<List<GroceryProperty>>{
override fun onFailure(call: Call<List<GroceryProperty>>, t: Throwable) {
_response.value = t.message
}
override fun onResponse(
call: Call<List<GroceryProperty>>,
response: Response<List<GroceryProperty>>
) {
_response.value="Success ${response.body()?.size} Grocery Property arrived"
}
})
}
override fun onCleared() {
super.onCleared()
}
}
The error you getting is Moshi telling you that it expects a JSON array, but it got an object. Your Retrofit endpoint method looks like this:
#GET("top_selling")
fun getProperties():
Call<List<GroceryProperty>>
Here, you are telling Retrofit that you expect a List. In JSON, this would be the array Moshi is expecting. However, upon clicking on the link to the endpoint you provided, the JSON you are receiving looks like this:
{
"status": "1",
"message": "top selling products",
"data": [
...
]
}
As you can see, this JSON is not an array but an object that contains an array, and that's where Moshi's error originates. To deserialize it into a List, it expected the begin of an array ([), but what it found was actually the begin of an object ({)
To sum it up, you are not expecting a List, but an object, which in turn contains that List (the data array in the JSON).
You would need to define another class that encapsulates this List, something the likes of this:
data class TopSellingResponse(
val status: String,
val message: String,
val data: List<GroceryProperty>
)
If you then change your method signature to
#GET("top_selling")
fun getProperties():
Call<TopSellingResponse>
Moshi should be able to deserialize the JSON object into your class and the contained data array into the List you initially expected.

Transition from AsyncTask to Coroutines in Retrofit: IllegalArgumentException

I keep getting error IllegalArgumentException when making minimal changes to transition from AsyncTask (before) to Kotlin Coroutines (after). Note that code is working as expected with AsyncTask.
Note: Retrofit is calling my own .php script that returns some object SimpleResultObject encoded in json String.
Before the change:
Retrofit:
#FormUrlEncoded
#POST("activity_signup.php")
fun activitySignUp(
#Field("activity_id") activityId: Int,
#Field("user_id") userId: Int) : Call<SimpleResultObject>
Activity (inside of AsyncTask):
#Override
protected doInBackground(...) {
val gson = GsonBuilder().setLenient().create()
val retrofit = Retrofit.Builder()
.baseUrl(LOCALHOST_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
val service = retrofit.create(RetrofitAPI::class.java)
val call = service.activitySignUp(activity_id, userId)
call.enqueue(Callback<SimpleResultObject>() {}
Receive object in #onResponse method and normally proceed futher.
After the change:
Retrofit:
#FormUrlEncoded
#POST("activity_signup.php")
suspend fun activitySignUp(
#Field("activity_id") activityId: Int,
#Field("user_id") userId: Int): SimpleResultObject
Activity:
fun signUp() {
myActivityScope.launch(Dispatchers.Main) {
val gson = GsonBuilder().setLenient().create()
val retrofit = Retrofit.Builder()
.baseUrl(LOCALHOST_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
val service = retrofit.create(RetrofitAPI::class.java)
try {
val result = service.activitySignUp(specificResultObject.activityId, userId)
} catch (t:Throwable)
Throws java.lang.IllegalArgumentException: No Retrofit annotation found. (parameter #3) for method RetrofitAPI.activitySignUpon service.activitySignUp line call
Note: myActivityScope is costum CoroutineScope that finished when hosting Activity finishes.
I have tried everything I could remember: adding OkHttpClient, changing to MoshiConverterFactory, trying other CoroutineScopes and Dispatchers, ...
EDIT: the problem might be on my .php side due to Exeption being above my argument number (maybe null result?), but don't know why something that worked before wouldn't work now.
Based on responses to the question I made a few modifications to the code and managed to fix the issue. The most important, as #Mohammad Sianaki pointed out, was rising Retrofit version from 25.0.0 to 26.0.0 that solved the problem.
So for everyone else that might get the IllegalArgumentException for the argument above the parameter number - consider checking Retrofit versions.
Special thanks to everyone that helped, especially to #CommonsWare!
The provided code in question has some structural issues.
First of all, it seems that a retrofit object is being created for each API call. So, it should be one for all API calls of the application.
Second, network operations should be executed in non-main threads. In the case of coroutines, they should be called in non-main contexts, like Dispatchers.IO.
Third, I think you should return a Response<SimpleResultObject> instead of SimpleResultObject in API functions.
Supposing above, I wrote some codes hoping to solve the problem. Because I think there are some hidden factors in question information.
build.gradle
dependencies {
implementation 'com.squareup.retrofit2:retrofit:2.6.1'
implementation 'com.squareup.retrofit2:converter-gson:2.6.1'
implementation 'com.squareup.okhttp3:logging-interceptor:3.14.1'
implementation 'com.squareup.okhttp3:okhttp-urlconnection:3.14.1'
implementation 'com.squareup.okhttp3:okhttp:3.14.1'
}
RetrofitAPI.kt
import retrofit2.Response
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.POST
interface RetrofitAPI {
#FormUrlEncoded
#POST("activity_signup.php")
suspend fun activitySignUp(
#Field("activity_id") activityId: Int,
#Field("user_id") userId: Int
): Response<SimpleResultObject>
// Other api declarations ...
}
BaseApiManager.kt
import okhttp3.JavaNetCookieJar
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.net.CookieManager
import java.util.concurrent.TimeUnit
abstract class BaseApiManager(endPoint: String) {
protected val retrofitAPI: RetrofitAPI =
createAdapter(endPoint)
.create(RetrofitAPI::class.java)
private fun createAdapter(choice: String): Retrofit {
return Retrofit.Builder()
.baseUrl(choice)
.client(createHttpClient())
.addConverterFactory(GsonConverterFactory.create()).build()
}
companion object {
private fun createHttpClient(): OkHttpClient {
val httpClient: OkHttpClient.Builder = OkHttpClient.Builder()
val cookieHandler = CookieManager()
val interceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.BODY
httpClient.interceptors().add(interceptor)
httpClient.cookieJar(JavaNetCookieJar(cookieHandler))
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
return httpClient.build()
}
}
}
ApiManager.kt
private const val API_END_POINT = "https://your.webservice.endpoint/"
object ApiManager : BaseApiManager(API_END_POINT) {
suspend fun activitySignUp(
activityId: Int,
userId: Int
) = retrofitAPI.activitySignUp(activityId, userId)
// Other api implementations ...
}
Usage:
fun signUp() {
myActivityScope.launch(Dispatchers.IO) {
ApiManager.activitySignUp(activityId, userId).also { response ->
when {
response.isSuccessful -> {
val result = response.body()
result?.apply {
// do something with the result
}
}
else -> {
val code = response.code()
val message = response.message()
// do something with the error parameters...
}
}
}
}
}

How to encode a field of an object as stringifed JSON instead of nested JSON object in Moshi?

I have a sealed class WebSocketMessage which has some subclasses. The WebSocketMessage has a field named type which is used for differentiating between subclasses.
All of the subclasses have their own field named payload which is of different type for each subclass.
Currently I am using Moshi's PolymorphicJsonAdapterFactory so that these classes can be parsed from JSON and encoded to JSON.
This all works, but what I need is to encode the the payload field to stringified JSON instead of JSON object.
Is there any possibility to write a custom adapter class to help me with this problem? Or is there any other solution so that I will not have to do this stringification manually?
I have tried looking into custom adapters but I can't find how I could pass moshi instance to adapter so that I can encode the given field to JSON and then stringify it, nor did I find anything else that could help me.
The WebSocketMessage class with its subclasses:
sealed class WebSocketMessage(
val type: Type
) {
enum class Type(val type: String) {
AUTH("AUTH"),
PING("PING"),
FLOW_INITIALIZATION("FLOW_INITIALIZATION")
}
class Ping : WebSocketMessage(Type.PING)
class InitFlow(payload: InitFlowMessage) : WebSocketMessage(Type.FLOW_INITIALIZATION)
class Auth(payload: Token) : WebSocketMessage(Type.AUTH)
}
The Moshi instance with PolymorphicJsonAdapterFactory:
val moshi = Moshi.Builder().add(
PolymorphicJsonAdapterFactory.of(WebSocketMessage::class.java, "type")
.withSubtype(WebSocketMessage.Ping::class.java, WebSocketMessage.Type.PING.type)
.withSubtype(
WebSocketMessage.InitFlow::class.java,
WebSocketMessage.Type.FLOW_INITIALIZATION.type
)
.withSubtype(WebSocketMessage.Auth::class.java, WebSocketMessage.Type.AUTH.type)
)
// Must be added last
.add(KotlinJsonAdapterFactory())
.build()
How I encode to JSON:
moshi.adapter(WebSocketMessage::class.java).toJson(WebSocketMessage.Auth(fetchToken()))
I currently get the JSON in the next format:
{
"type":"AUTH",
"payload":{
"jwt":"some_token"
}
}
What I would like to get:
{
"type":"AUTH",
"payload":"{\"jwt\":\"some_token\"}"
}
In the second example the payload is a stringified JSON object, which is exactly what I need.
You can create your own custom JsonAdapter:
#Retention(AnnotationRetention.RUNTIME)
#JsonQualifier
annotation class AsString
/////////////////////
class AsStringAdapter<T>(
private val originAdapter: JsonAdapter<T>,
private val stringAdapter: JsonAdapter<String>
) : JsonAdapter<T>() {
companion object {
var FACTORY: JsonAdapter.Factory = object : Factory {
override fun create(
type: Type,
annotations: MutableSet<out Annotation>,
moshi: Moshi
): JsonAdapter<*>? {
val nextAnnotations = Types.nextAnnotations(annotations, AsString::class.java)
return if (nextAnnotations == null || !nextAnnotations.isEmpty())
null else {
AsStringAdapter(
moshi.nextAdapter<Any>(this, type, nextAnnotations),
moshi.nextAdapter<String>(this, String::class.java, Util.NO_ANNOTATIONS)
)
}
}
}
}
override fun toJson(writer: JsonWriter, value: T?) {
val jsonValue = originAdapter.toJsonValue(value)
val jsonStr = JSONObject(jsonValue as Map<*, *>).toString()
stringAdapter.toJson(writer, jsonStr)
}
override fun fromJson(reader: JsonReader): T? {
throw UnsupportedOperationException()
}
}
/////////////////////
class Auth(#AsString val payload: Token)
/////////////////////
.add(AsStringAdapter.FACTORY)
.add(KotlinJsonAdapterFactory())
.build()

Android Kotlin Dagger2 provide gson : Parameter specified as non-null is null

i want to provide a single gson instance, so i provide it in my NetworkModule, when i create retrofit api, gson is non null, but when i use gson instance in retrofit class, it is null..
#Module
class NetworkModule {
#Provides
#Singleton
fun provideGson(): Gson {
return Gson()
}
#Provides
#Singleton
fun provideMyEnterpriseApi(gson: Gson): MyEnterpriseApi {
//HERE GSON IS NOT NULL
return RetrofitMyEnterpriseApi(BuildConfig.API_MYENTERPRISE_URL,
BuildConfig.API_MYENTERPRISE_CONNECTION_TIMEOUT,
BuildConfig.API_MYENTERPRISE_READ_TIMEOUT,
gson)
}
}
My retrofit api class :
class RetrofitMyEnterpriseApi(baseUrl: String, connectTimeout: Long, readTimeout: Long, private val gson: Gson)
: RetrofitApi<RetrofitMyEnterpriseCalls>(baseUrl, connectTimeout, readTimeout, RetrofitMyEnterpriseCalls::class.java) {
override fun buildRetrofit(baseUrl: String, connectTimeout: Long, readTimeout: Long): Retrofit {
val builder = Retrofit.Builder()
builder.baseUrl(baseUrl)
builder.client(buildClient(connectTimeout, readTimeout))
builder.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
builder.addConverterFactory(GsonConverterFactory.create(gson))
return builder.build()
}
private fun buildClient(connectTimeout: Long, readTimeout: Long): OkHttpClient {
//HERE GSON IS NULL
return OkHttpClient.Builder()
.addInterceptor(OAuth2Interceptor(gson, this))
.addInterceptor(LoggingInterceptor.newInstance())
.connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
.readTimeout(readTimeout, TimeUnit.MILLISECONDS)
.build()
}
}
RetrofitApi class:
abstract class RetrofitApi<C>
(baseUrl: String, connectTimeout: Long, readTimeout: Long, callsClass: Class<C>) {
protected val calls: C
init {
val retrofit = buildRetrofit(baseUrl, connectTimeout, readTimeout)
calls = retrofit.create(callsClass)
}
protected abstract fun buildRetrofit(baseUrl: String, connectTimeout: Long, readTimeout: Long): Retrofit
}
So, when i provide it's not null, but when i use it in buildClient method is null, i can't see why, i miss something in Kotlin maybe.
I found answer when i posted \o/
So the problem was gson field in RetrofitMyEnterpriseApi class is instancied after the init of RetrofitApi parent class, i think it's strange because i pass it in parameters but..
I just need to call my buildRetrofit method after init of RetrofitMyEnterpriseApi class, or send gson instance to my parent class.

Categories

Resources