Retrofit 2 : Custom annotations for custom interceptor - android

I have custom interceptor for authentication:
#Named("authInterceptor")
#Provides
#Singleton
fun providesAuthInterceptor(preferencesManager: PreferencesManager): Interceptor {
return Interceptor { chain ->
val newBuilder = chain.request().newBuilder()
newBuilder.addHeader("access-token", preferencesManager.getAccessToken())
val request = newBuilder.build()
return#Interceptor chain.proceed(request)
}
}
But I have some calls that not need auth header.
What I would like to have in my service is:
interface NetService {
#NEEDAUTH
#GET("users")
fun getAllShops(key: String): Single<SomeResponse>
#FormUrlEncoded
#POST("users")
fun register(#Field("nickname") nickname: String): Single<SomeResponse>
}
So, the first call will use authInterceptor, the second one will not use it.

Since the version of Retrofit 2.6.0, you can get the annotations in OkHttp Interceptor using the tag field like this:
response.request.tag(Invocation::class.java)?.method()?.getAnnotation(YourAnnotation::class.java)
Then inside of the interceptor, you can verify if the request is annotated or no.
Retrofit Changelog:
New: #Tag parameter annotation for setting tags on the underlying OkHttp Request object. These can be read in CallAdapters or OkHttp Interceptors for tracing, analytics, varying behavior, and more.
https://github.com/square/retrofit/pull/2899/files

I have similar requirement, what I found is Annotation can be read in Converter.Factory:requestBodyConverter(),
Converter.Factory:responseBodyConverter() and CallAdapter.Factory.get().
I also found two articles as examples for implementation on each way.
Using Converter: Auto Caching with Retrofit
We’ll use the gson converter (GsonConverterFactory) provided by Retrofit and modify it slightly to include a listener in GsonResponseBodyConverter.class which handles the http response parsing.
In GsonCacheableConverter, it overrides responseBodyConverter() to persist response tagged with #Cacheable.
Using CallAdapter: Custom Annotations with Retrofit 2
We read the annotation in the CallAdapter.Factory and when the request gets created in the CallAdapter, we will store some information for this kind of request within some map, to identify it later in some interceptor.
It uses a custom CallAdapter to get annotation #Authenticated, and put data into a map, which later parsed in the Interceptor.
I think requestBodyConverter() and CallAdapter are closer to your requirement.
While if you do not insist on custom annotations, the easiest way for now in my opinion is to add custom header to the api interface, then read and remove it in the interceptor.
That is, adding #Headers("needauth: 1") to your services, and using chain.request().header("needauth") to get the value.
Example: Sneaking Data into an OkHttp Interceptor.

Interceptors are concepts that exist in OkHttp, Retrofit knows nothing about them.
What you need to do is have two OkHttp clients, with their respective instances of Retrofit.
One with the authentications headers
One for the rest
Whether you need the authentication headers or not will decide which instance to inject.

Based on Max Cruz answer I'm using as extension function:
fun <T : Annotation> Request.getCustomAnnotation(annotationClass: Class<T>): T? = this.tag(Invocation::class.java)?.method()?.getAnnotation(annotationClass)
And you can use then like that:
request.getCustomAnnotation(YourAnnotation::class.java)

Related

Retrofit looking for non-existent param?

I'm using retrofit to generate the POST requests in my simple API call:
interface IRetrofitCommService {
#POST("pushnotifications/{entityId}")
suspend fun getNotificationsAsync(
#Path("entityId") entityId: Long,
#Body model: GetNotificationsDto
): Response<List<NotificationData>>
#POST("pushnotifications")
suspend fun registerDeviceAsync(#Body model: RegisterEntityDto):
Response<RegisterEntityResultDto>
}
Note that in the second call, I have only 1 parameter, which is marked with the #Body annotation.
Yet, when I try to use the web call, I get this exception: No annotation found for param 2
Here is my factory for creating the call:
object RetrofitFactory {
const val BASE_URL = "https://localhost:5051/api/"
fun createService(): IRetrofitCommService {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(IRetrofitCommService::class.java)
}
}
Here is the DTO in question:
data class RegisterEntityDto(val name: String, val eventType: Short, val clientId: String)
Why is it looking for a 2nd parameter then? What am I missing?
I'm not very familiar with the Retrofit and I'm doing an educated guess here, but after a short discussion in comments it seems my understanding is actually correct.
Internally, suspend functions in Kotlin receive additional Continuation parameter. It is always the last parameter and it is hidden from the Kotlin code. However, if we look into the generated bytecode, we'll see registerDeviceAsync() function actually receives 2 parameters.
If some tool we use is not aware of suspend functions, it won't be able to properly interpret this additional parameter. It will just "think" registerDeviceAsync() has two params. Retrofit added support for suspend functions in version 2.6.0, so I guess if using older versions with suspend functions we will get exactly the behavior you faced.
You just need to update Retrofit to newer version.

Retrofit - Is it possible to avoid calling actual api from application interceptor and return hard-coded response?

I understand that this is how the interceptor works and a request from the application passes through the OkHttp core, via retrofit wrapper and OkHttpp core call to make an actual network request and the network response to the application interceptor response via the retrofit wrapper.
Is there a way to avoid calling the actual request from the application interceptor, as in, in some scenario in application interceptor check if the request URL is some string, then, in that case, do-not-call the actual network request and directly return the hard-coded response from the application interceptor?
You can return a new Response instead of calling chain.proceed() and it would stop the chain from moving forward. You can do it like this.
if(something)
return Response.Builder()
.code(200) //Or whatever you might later check from
.protocol(Protocol.HTTP_2) //or 1
.message("SUCCESS")
.body(ResponseBody.create(MediaType.get("application/json"), "{\"x\": 1}")) // your response
.request(chain.request())
.build()
I also recommend to define an annotation, and get it in your interceptor instead of checking for the URL.
request.tag(Invocation::class.java)?.method()?.getAnnotation(YourAnnotation::class.java)
Retrofit has so called "retrofit-mock", which is designed specifically for your task - mocking:
https://github.com/square/retrofit/tree/master/retrofit-mock
You can try it, maybe you will find it useful.
Example of usage:
https://github.com/square/retrofit/blob/master/samples/src/main/java/com/example/retrofit/SimpleMockService.java
You can create 2 implementations of your retrofit service - real and mocked. And provide one of them via DI depending on build flavor or application mode (demo mode or real http session).

Inject retrofit using dagger using `base_url` from firebase remote config

I know that we can inject retrofit using dagger in the following manner when the BASE_URL is fixed:
#Provides
#Singleton
Retrofit provideRetrofit(Gson gson, OkHttpClient okHttpClient) {
Retrofit retrofit = new Retrofit.Builder().baseUrl(BASE_URL)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson)).build();
return retrofit;
}
#Provides
#Singleton
ApiCallInterface provideApiCallInterface(Retrofit retrofit) {
return retrofit.create(ApiCallInterface.class);
}
But, the problem here is my BASE_URL comes from firebase remote config, and I can obtain that only after runtime which is too late as dagger will initialize retrofit before that. How can this problem be solved? All the tutorials and examples that I see, work with static base urls and hence dont encounter this problem.
I am using remoteconfig for my base url so that I can control the app environment without pushing in app updates.
Thanks in advance.
According to Retrofit docs
About base_URL:
Endpoint values may be a full URL.
Values which have a host replace the host of baseUrl and values also
with a scheme replace the scheme of baseUrl.
Examples:
Base URL: http://example.com/
Endpoint: https://github.com/square/retrofit/
Result: https://github.com/square/retrofit/
So if endpoint's URL holds full URL, base URL will be replaced|ignored
About GET-annotation (the same with POST):
A relative or absolute path, or full URL of the endpoint. This value
is optional if the first parameter of the method is annotated with
#Url.
So with #URL parameter of your endpoint and empty GET parameter you can set full URL in runtime with parameter #URL of your endpoint
So try with something like this in your endpoint:
#GET()
fun getData(#Url url : String): Call<SomeResult>
and set parameter of method (full URL) in runtime
I found a workaround by leaving the Firebase Remote Config version to 17.0.0 and using the FirebaseRemoteConfig#setDefaults function.
build.gradle
implementation 'com.google.firebase:firebase-config:17.0.0'
Class. Note the setConfigSettingsAsync is still async, setDefaults stays sync.
private final FirebaseRemoteConfig mFirebaseRemoteConfig;
mFirebaseRemoteConfig.setConfigSettingsAsync(new FirebaseRemoteConfigSettings.Builder...);
mFirebaseRemoteConfig.setDefaults(R.xml.remote_config_defaults);
mFirebaseRemoteConfig.fetchAndActivate().addOnCompleteListener(...)
In addition to the answer above by sergiy tikhonov you can also use an interceptor to intercept your requests and swap the host with your firebase remote config's BASE_URL.
See this gist for an example.

How to have multiple base URL in retrofit2 using Kotlin and Kodein structure

I have followed THIS tutorial for MVVM and Retrofit2 with Kodein structure/framework. I wanted to know, Using this same framework/structure how can i have multiple base URLs in a single App.
Below is the code of "MyApi" interface which have an interceptor class as parameter.
companion object {
operator fun invoke(
networkConnectionInterceptor: NetworkConnectionInterceptor
): MyApi {
val okkHttpclient = OkHttpClient.Builder()
.addInterceptor(networkConnectionInterceptor)
.readTimeout(20, TimeUnit.SECONDS)
.build()
return Retrofit.Builder()
.client(okkHttpclient)
.baseUrl("http://my-base-url")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(MyApi::class.java)
}
}
And here is how Iam initializing MyApi interface in Application class:
bind() from singleton { PreferenceProvider(instance()) }
bind() from singleton { NetworkConnectionInterceptor(instance(), instance()) }
bind() from singleton { MyApi(instance()) }
Here instance() in MyApi is obviously NetworkConnectionInterceptor.
I have seen a lot of examples on stackoverflow and medium but i didnt got any help.
I think i found a work around to achive this. There are two solutions...
First Solution:
You can create a new interface for other microservice (base url) and use it just like the first one. Now there are some pros and cons for this.
Pros:
Both the interfaces will be independent of each other.
You can use either of the interface or both in same activity as you need.
Cons:
If there is another microservice poped up , you have to create one more interface for that. 😿
If you even have to 2 microservice and you have to run same application on development and qa server providing an option for testers and developers to switch between qa and dev servers at run time the you need to have 4 interfaces and 2 extra for production that means 6 interfaces, It will become very had to manage it.
Second Solution:
You can use #URL annotation provided by retrofit2. Now if you do this there will be no base_url, you have to pass the URL and microservice name in a common function which will return you a full URL based on what server users/testers are on (dev/qa or prod).
Pros:
No extra interfaces, Only one will work out.
Easy management of all the API calls because of the common function.
Cons:
You have to call the common funtion in #URL annotation in each and every API call.
I dont see any more. 😊

Retrofit GET without a value in Android

I use Retrofit for most of my calls but in one of the cases, I have the full path provided in arguments. My URL is like this http://www.example.com/android.json. This URL is provided in full so I have to path it at runtime. I implement endpoint as suggested here
https://medium.com/#kevintcoughlin/dynamic-endpoints-with-retrofit-a1f4229f4a8d
but in the #GET I need to be able to put #GET(""). This does not work as I get an error saying I should provide at least one "/".
If I add the slash the URL becomes http://www.example.com/android.json/ and it does not work, the server returns forbidden. I also tried creating a custom GET interface similar to here https://github.com/square/retrofit/issues/458 but with GET and without providing a value method in the interface. Then I get another error saying value is missing.
Basically I need to be able to provide an empty or null value but retrofit does not allow that. How could I solve this problem? For now I am doing the JSON request manually but is there a way I could use retrofit for this case? I need to pass the full URL there is no way I can do endpoint http://www.example.com and #GET("/android.json").
Thanks
You can use #GET(".") to indicate that your url is the same as the base url.
#GET(".")
Observable<Result> getData(#Query("param") String parameter);
I've tried this approach, however didn't work for me.
Workaround for this issue is:
//Retrofit interface
public interface TestResourceClient {
#GET
Observable<Something> getSomething(#Url String anEmptyString);
}
//client call
Retrofit.Builder().baseUrl("absolute URL").build()
.create(TestResourceClient.class).getSomething("");
The downside of this solution is that you have to supply empty string in getSomething("") method call.
I face the same problem with Retrofit 2. Using #GET, #GET("") and #GET(".") not solved my problem.
According to the official document you can the same baseUrl and #GET argument.
Endpoint values may be a full URL.
Values that have a host replace the host of baseUrl and values also with a scheme replace the scheme of baseUrl.
Base URL: http://example.com/
Endpoint: https://github.com/square/retrofit/
Result: https://github.com/square/retrofit/
So in my case:
interface MyAPI {
#GET("http://www.omdbapi.com/")
suspend fun getMovies(
#Query("apikey") apikey: String,
#Query("s") s: String
): Response<MoviesResponse>
companion object {
operator fun invoke(): MyAPI {
return Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl("http://www.omdbapi.com/")
.build()
.create(MyAPI::class.java)
}
}
}

Categories

Resources