Chinthaka Devinda
Chinthaka Devinda

Reputation: 1085

Retrofit generic service interface

I am creating a generic API layer for Retrofit

Here is my service class:

public interface ApiService {

    @POST("api/authenticate")
    Call<Class> postData(@Body Class postBody);

}

public  void  postRequest(String actionUrl,GenericModelClass postBodyModel){
    mApiService.postData(postBodyModel.getClass()).enqueue(new Callback<Class>() {


        @Override
        public void onResponse(Call<Class> call, Response<Class> response) {
            response.getClass().getComponentType();

            Log.d("TFFF", response.toString());
        }

        @Override
        public void onFailure(Call<Class> call, Throwable t) {
          Log.d("TFFF", t.toString());
        }
    });
}

But this one gives me:

java.lang.UnsupportedOperationException: Attempted to serialize java.lang.Class: a2a.rnd.com.a2ahttplibrary.retrofit.model.User. Forgot to register a type adapter?

I want to get the User type from the generic type but I am getting this exception.

Upvotes: 2

Views: 13072

Answers (5)

porwalankit
porwalankit

Reputation: 481

Try this in your APIManager class

suspend inline fun <reified RS : Any> postRequest(
    @Url url: String,
    @Body body: RequestBody,
    @HeaderMap headerMap: Map<String, String>
): RS = Gson().fromJson(apiService.postRequest(url, body, headerMap).body().toString(), RS::class.java)

Upvotes: 0

murat_yuksektepe
murat_yuksektepe

Reputation: 61

After searching for a long time and i finally found the correct way. You can use these codes for send GET and POST requests with totally generic url and parameters.

Ofcourse you have to implement some dependencies for using this (eg. Retrofit, Gson, Hilt...) and you have to edit somethings.

Have a nice coding ✌🏻


HttpApiModule.kt

@Module
@InstallIn(SingletonComponent::class)
object HttpApiModule {

    @Provides
    @Singleton
    fun provideRetrofit(): Retrofit {
        val interceptor = HttpLoggingInterceptor()
        interceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
        val client = OkHttpClient.Builder().addInterceptor(interceptor).build()
        return Retrofit.Builder()
            .baseUrl("http://192.168.1.25:8000/")
            .client(client)
            .addConverterFactory(GsonConverterFactory.create())
            .addCallAdapterFactory(CoroutineCallAdapterFactory())
            .build()
    }


    @Provides
    @Singleton
    fun provideHttpApiService(retrofit: Retrofit): HttpApiService = retrofit.create()
}

HttpApiService.kt

interface HttpApiService {
    @POST
    suspend fun <R : Any> doPostRequest(
        @Header("Authorization") token: String,
        @Url path: String,
        @Body inputModel: JsonObject
    ): Response<R>


    @GET
    suspend fun <R : Any> doGetRequest(
        @Header("Authorization") token: String,
        @Url path: String,
        @QueryMap params: Map<String, String>
    ): Response<R>
}

DataModelExtension.kt

val gson = Gson()

//convert a data class to a map
fun <T> T.serializeToMap(): Map<String, String> {
    return convert()
}

//convert a map to a data class
inline fun <reified T> Map<String, String>.toDataClass(): T {
    return convert()
}

//convert an object of type I to type O
inline fun <I, reified O> I.convert(): O {
    val json = gson.toJson(this)
    return gson.fromJson(json, object : TypeToken<O>() {}.type)
}

HttpRequest.kt

class HttpRequest @Inject constructor(
    private val httpApiService: HttpApiService,
) {
    suspend fun <RQ : Any, RS : Any> postRequest(
        context: Context,
        path: String,
        requestModel: RQ
    ): Flow<Resource<RS>> = flow {
        val gson = Gson()
        val bodyJson = gson.toJson(requestModel)
        val jsonObject = gson.fromJson(bodyJson, JsonObject::class.java)
        coroutineScope {

            emit(Resource.Loading)

            if (context.isOnline()) {
                val call = httpApiService.doPostRequest<RS>(
                    inputModel = jsonObject,
                    token = "[TOKEN]",
                    path = path
                )
                call.run {
                    if (call.isSuccessful) {
                        body()?.let {
                            emit(Resource.Success(it))
                        } ?: kotlin.run {
                            emit(Resource.Error(BaseError(errorMessage = call.message())))
                        }
                    } else {
                        emit(Resource.Error(BaseError(errorMessage = "The http request is not successful!")))
                    }
                }
            } else {
                emit(Resource.Error(BaseError(errorMessage = "Internet connection error!")))
            }

        }
    }

    suspend fun <RQ : Any, RS : Any> getRequest(
        context: Context,
        path: String,
        requestModel: RQ
    ): Flow<Resource<RS>> = flow {
        val params = requestModel.serializeToMap()
        coroutineScope {

            emit(Resource.Loading)

            if (context.isOnline()) {
                val call = httpApiService.doGetRequest<RS>(
                    params = params,
                    token = "[TOKEN]",
                    path = path
                )
                call.run {
                    if (call.isSuccessful) {
                        body()?.let {
                            emit(Resource.Success(it))
                        } ?: kotlin.run {
                            emit(Resource.Error(BaseError(errorMessage = call.message())))
                        }
                    } else {
                        emit(Resource.Error(BaseError(errorMessage = "The http request is not successful!")))
                    }
                }
            } else {
                emit(Resource.Error(BaseError(errorMessage = "Internet connection error!")))
            }
        }
    }
}

YourViewModel.kt

@HiltViewModel
class DashboardViewModel @Inject constructor(
    private val httpApiService: HttpApiService,
    @SuppressLint("StaticFieldLeak") @ApplicationContext private val context: Context
) : BaseViewModel() {

    suspend fun httpRequest() {
        val httpRequest = HttpRequest(httpApiService)

        // Get
        httpRequest.getRequest<QuestionsRequest, QuestionsResponse>(
            context = context,
            path = "api/v1/get/questions",
            requestModel = QuestionsRequest()
        ).collectLatest {
            log("getRequest state: $it")
        }
        
        // Post
        httpRequest.postRequest<QuestionsRequest, QuestionsResponse>(
            context = context,
            path = "api/v1/get/questions",
            requestModel = QuestionsRequest()
        ).collectLatest {
            log("postRequest state: $it")
        }
    }

    data class QuestionsRequest(
        val count: 7,
    )

    data class QuestionsResponse(
        val success: Boolean,
        val size: Int,
        val questions: List<QuestionResponse>
    )

}

Fyi @compaq-le2202x

Upvotes: 0

Uzair Khan
Uzair Khan

Reputation: 81

I think I'm late on this but my solution may help new programmers as it's the shortest that i have come across. So here it goes:

  1. Instead of using a specific class as input parameter in service, we will use JsonObject from com.google.gson

  2. Then we will use a helper function that will call that service, helper function will accept our model and serialize it into equivalent json object,

  3. After that we will convert that json object to JsonObject using gson deserialization.

  4. Pass that parameter to the service and it will work perfectly.

    @POST
    suspend fun <V : Any> doPostRequest(
    @Header("Authorization") token: String = getToken(),
    @Url url: String,
    @Body inputModel: JsonObject
    ): Response<V>
    

this is how our service function will look like, then I placed my helper function in the base repository.

suspend fun <T : Any, V : Any> postRequest(
    apiName: String,
    model: T
): Flow<Resource<V>> {
    val gson = Gson()
    val body = gson.toJson(model)
    val jsonObject = gson.fromJson(body, JsonObject::class.java)
    return safeApiCall {
        sharedApiService.doPostRequest(
            url = apiName,
            inputModel = jsonObject
        )
    }
}

Upvotes: 2

Lyubomyr Shaydariv
Lyubomyr Shaydariv

Reputation: 21105

You're doing it in a way that does not make sense, and this is why you're getting:

java.lang.UnsupportedOperationException: Attempted to serialize java.lang.Class: a2a.rnd.com.a2ahttplibrary.retrofit.model.User. Forgot to register a type adapter?

Your service does not specify a type parameter. Class handles quite another purpose: it's an object that represents a class loaded by JVM. Serializing and deserializing Class instances makes really tiny sense if any, and that's why Gson does not provide it. All you want is generic methods. There are myriad articles for this subject over Internet.

Next, Retrofit does not work with method type parameters to simplify the type analysis under the hood dramatically. That's fine.

@GET("/")
<T> Call<T> get();

This won't work. How would you pass necessary type information data then? The only way to pass that info I can think of is introducing a wrapper to hold both value and its type (or type token to simplify Gson).

final class GenericBody<T> {

    final T body;
    final TypeToken<T> typeToken;

    GenericBody(final T body, final TypeToken<T> typeToken) {
        this.body = body;
        this.typeToken = typeToken;
    }

}

Then an example service might be declared as follows:

interface IGenericService {

    @POST("/")
    Call<Void> post(@Body @SuppressWarnings("rawtypes") GenericBody genericBody);

}

Here, the Call is declared to return nothing, and genericBody is intentionally made raw-typed to let it pass Retrofit validation.

Next, the Gson part.

final class GenericBodyTypeAdapterFactory
        implements TypeAdapterFactory {

    private static final TypeAdapterFactory genericBodyTypeAdapterFactory = new GenericBodyTypeAdapterFactory();

    private GenericBodyTypeAdapterFactory() {
    }

    static TypeAdapterFactory getGenericBodyTypeAdapterFactory() {
        return genericBodyTypeAdapterFactory;
    }

    @Override
    public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
        if ( !GenericBody.class.isAssignableFrom(typeToken.getRawType()) ) {
            return null;
        }
        final TypeAdapter<GenericBody<T>> genericBodyTypeAdapter = new TypeAdapter<GenericBody<T>>() {
            @Override
            public void write(final JsonWriter out, final GenericBody<T> value)
                    throws IOException {
                final T body = value.body;
                final TypeAdapter<T> typeAdapter = gson.getDelegateAdapter(GenericBodyTypeAdapterFactory.this, value.typeToken);
                typeAdapter.write(out, body);
            }

            @Override
            public GenericBody<T> read(final JsonReader in) {
                throw new UnsupportedOperationException();
            }
        };
        @SuppressWarnings("unchecked")
        final TypeAdapter<T> typeAdapter = (TypeAdapter<T>) genericBodyTypeAdapter;
        return typeAdapter;
    }

}

What it does it is:

  • checks if it can handle GenericBody instances;
  • resolves appropriate type adapters for the <T> by the bound type token;
  • writes the generic body value to the output.

No read is implemented.

Example of use (full of mocks (staticResponse(applicationJsonMediaType, "OK")) that can be easily translated to your code):

private static final TypeToken<List<String>> stringListTypeToken = new TypeToken<List<String>>() {
};

private static final Gson gson = new GsonBuilder()
        .registerTypeAdapterFactory(getGenericBodyTypeAdapterFactory())
        .create();

private static final OkHttpClient client = new OkHttpClient.Builder()
        .addInterceptor(staticResponse(applicationJsonMediaType, "OK"))
        .build();

private static final Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("http://whatever")
        .client(client)
        .addConverterFactory(GsonConverterFactory.create(gson))
        .build();

private static final IGenericService genericService = retrofit.create(IGenericService.class);

public static void main(final String... args)
        throws IOException {
    final GenericBody<List<String>> body = new GenericBody<>(asList("foo", "bar", "baz"), stringListTypeToken);
    genericService.post(body).execute();
}

This would write ["foo","bar","baz"] to the output stream respecting properly configured Gson (de)serialization strategies.

Upvotes: 17

Sandeep dhiman
Sandeep dhiman

Reputation: 1921

In think you are missing below line of code in your postrequest method

ApiService mApiService = APIUtil.getApiService().create(ApiInterface.class);

Upvotes: -3

Related Questions