Reputation: 1085
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
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
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
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:
Instead of using a specific class as input parameter in service, we will use JsonObject from com.google.gson
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,
After that we will convert that json object to JsonObject using gson deserialization.
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
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:
GenericBody
instances;<T>
by the bound type token;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
Reputation: 1921
In think you are missing below line of code in your postrequest method
ApiService mApiService = APIUtil.getApiService().create(ApiInterface.class);
Upvotes: -3