I have created below module in my android application.
val appNetworkModule = module {
// Dependency: OkHttpClient
single {
var okHttpClient=OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.addInterceptor(get<Interceptor>("LOGGING_INTERCEPTOR"))
.addInterceptor(get<Interceptor>("OK_HTTP_INTERCEPTOR"))
.build()
}
}
Now, I have to clear all the previous api calls while doing Logout from the app.
So, I need to access the variable used in here as above named : okHttpClient
I am trying to access it as below to cancel all the previous api calls in my main activity:
appNetworkModule.okHttpClient.dispatcher.cancelAll()
But, okHttpClient is not accessible.
What might be the issue?
I might be late here, but the correct answer is that since it is defined in the module (Provided that you are using koin as a DI library). We need to inject it in our required class (where we need to call the okHttpClient) either using a constructor injection or using a field injection.
So our code will be something like this.
Using Construction Injection: Make sure your Class TestClass gets the injected okHttpClient for example define the TestClass in the module like this single { TestClass(get()) }
class TestClass(private val okHttpClient: OkHttpClient) {
fun clearAllApis(){
okHttpClient.dispatcher.cancelAll()
}
}
Using field injection: If you need to use field injection instead
class TestClass {
private val okHttpClient : OkHttpClient by inject() //If it is extended from Android platform class like AppCompatActivity, Fragment etc. or a KoinComponent extended class
private val okHttpClient : OkHttpClient by inject(OkHttpClient::class.java) //Otherwise
fun clearAllApis(){
okHttpClient.dispatcher.cancelAll()
}
}
Make sure .inject() is from org.koin.android.ext.android.inject
Related
I have my Retrofit and OkHttp instances injected. I am changing the headers of my OkHttp object using an Interceptor. In this Interceptor, I set information such as app name, version etc.
I am now adding a login and want to set an authToken to the headers. However, this only exists AFTER login, obviously. So what I want to do is be able to add a new header after login for authToken. However, I can't get access to my OkHttp object as its buried in my module, and I just get my RestClient object injected in my class, with everything else built.
I was thinking of copying all my provides... code and making duplicates for before an after Login. For example, wrap my RestClient in a class called "PreLoginApi" that doesn't have authToken, and then creating another wrapper class called "PostLoginApi" which has the authToken, but this won't work if Dagger creates the objects on app launch.
#Provides
#Singleton
#Named("HeaderInterceptor")
fun provideHeaderInterceptor(context: Context): Interceptor {
val headerMap = context.getHeaderMap()
return Interceptor { chain ->
val original = chain.request()
val builder = original.newBuilder()
headerMap.keys.forEach {
builder.addHeader(it, headerMap[it] ?: "")
}
val request = builder.build()
chain.proceed(request)
}
}
#Provides
fun providesOkHttpClient(
#Named("HeaderInterceptor") headerInterceptor: Interceptor,
): OkHttpClient {
val builder = OkHttpClient.Builder()
.connectTimeout(60, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.addInterceptor(headerInterceptor)
return builder.build()
}
You can see here where I call my extension function Context.getHeaderMap(). Inside this function, I'm retrieving the authToken from EncryptedSharedPreferences.
So ideally, I'd like to be able to tell Dagger to redo this initialisation once I login, because at this stage the authToken will be set. Is it possible to just re-initialise this one module or will I have to wrap my RestClient in another class, and maybe use Lazy loading to initialise the "PostLoginApi"?
Thanks.
EDIT:
I do similar for Firebase FCM token, where I pass in the headerMap during initialisation, like this:
private fun updateFirebaseToken(headerMap: HashMap<String, String>) {
FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task ->
if (!task.isSuccessful) {
return#OnCompleteListener
}
// Get new FCM registration token
task.result?.let { token ->
headerMap["FirebaseId"] = token
}
})
}
But I don't think I can do something like this for a SharedPrefs value.
I am using firebase id token as my authorization header for http requests. To get this token from android client requires a method: user.getIdToken(true) which is a callback based async function.
I also have a hilt module that provides an instance of a retrofit service (interface):
Here is the service interface:
interface CountriesService {
#GET("countries")
suspend fun getCountries(): Response<CountriesResponse>
}
Here is the Hilt module that provides this service:
#Module
#InstallIn(SingletonComponent::class)
object CountriesModule {
private const val CLOUD_RUN_BASE_URL = "https://my-base-url/"
#Provides
fun provideCountriesService(): CountriesService {
return Retrofit.Builder().apply {
baseUrl(CLOUD_RUN_BASE_URL)
addConverterFactory(GsonConverterFactory.create())
}.build().create(CountriesService::class.java)
}
}
Now I need to add an OkHttp inteceptor (Simply adds authorization header to all requests) to the above module like: Interceptor:
var client: OkHttpClient = OkHttpClient.Builder().addInterceptor { chain ->
val user = FirebaseAuth.getInstance().currentUser
user?.getIdToken(true)?.addOnCompleteListener { task ->
if(task.isSuccessful){
val token = task.result.token
val newRequest: Request = chain.request().newBuilder()
.addHeader("Authorization", "Bearer $token")
.build()
chain.proceed(newRequest) // return not allowed here
} else{
Log.d("id_token", task.exception?.message.toString())
}
}
}.build()
I then pass the client value as a parameter to the client() method in the module below:
#Module
#InstallIn(SingletonComponent::class)
object CountriesModule {
private const val CLOUD_RUN_BASE_URL = "https://my-base-url/"
#Provides
fun provideCountriesService(): CountriesService {
return Retrofit.Builder().apply {
client(client) // Newly added line
baseUrl(CLOUD_RUN_BASE_URL)
addConverterFactory(GsonConverterFactory.create())
}.build().create(CountriesService::class.java)
}
}.
The problem with the interceptor code is that one cannot return the value chain.proceed(newRequest) from the addOnCompleteListener{} block.
Is there a better way to achieve this. How can I make a callback based method return a value that is later consumed by another method?
You have to rethink your initialization and initialize the HTTP client, as well as move all the code that relies on it, to a later phase of your application. On Android there's only a limited set of things you can do synchronously inside initialization callbacks.
Typically applications start in a phase that just shows a splash screen while they are doing initialization that requires the network.
You need to rethink how you do the calls, the order of calls in particular. Probably in your interceptor you should use the cached token, but prior that, you need to do invalidation of the token. This will result your interceptor always use the latest token.
Alternatively, you can insist on firebase Task delivers the result blocking, you HTTP calls are always on background thread, but I wouldn't recommend.
I am using the new Retrofit2 with suspending coroutines, and with GET requests everything works fine.
But I now have to implement a POST request, and just can't get it to work
I have a CURL example that looks like this:
curl -X POST -H "Content-Type: application/json;charsets: utf-8" -d '{"tx_guapptokenlist_tokenitem":{"tokenchar":"my-token-string","platform":"android"}}' https://www.example-url.com/tokens?type=56427890283537921
This works fine, and returns this response: {"errors":false,"success":true}%
So here's what my request looks like in my Api class right now:
#Headers( "Content-Type: application/json" )
#POST("/tokens?type=56427890283537921")
suspend fun sendFirebaseToken(#Body tokenRequest: RequestBody) : Call<TokenResponse>
This is my TokenResponse class:
#JsonClass(generateAdapter = true)
data class TokenResponse(
#Json(name="errors")
val errors: Boolean,
#Json(name="success")
val success: Boolean)
and the ApiClient class I'm using:
object ApiClient {
private const val BASE_URL = "https://myExampleUrl.com"
private var retrofit: Retrofit? = null
var moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
val client: Retrofit?
get() {
if (retrofit == null) {
retrofit = Retrofit.Builder().baseUrl(
BASE_URL
).client(getOkHttpClient())
.addConverterFactory(MoshiConverterFactory.create())
.build()
}
return retrofit
}
fun getOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder().addInterceptor(getLoggingInterceptor())
.connectTimeout(120, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS).writeTimeout(90, TimeUnit.SECONDS).build()
}
private fun getLoggingInterceptor(): HttpLoggingInterceptor {
return HttpLoggingInterceptor().setLevel(
if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.HEADERS
else HttpLoggingInterceptor.Level.NONE
)
}
}
The first odd thing I noticed: Even with the #POST annotation, if my suspend fun has no return type, I get no error, but okhttp will always send a GET request (at least the endpoint always receives a GET). Not sure if that is supposed to be like that?
Anyway: I need the return values, so I'm returning Call<TokenResponse>.
This leads me to my main problem, that I can't solve: If now I execute my code, it crashes with this log:
java.lang.IllegalArgumentException: Unable to create converter for retrofit2.Call<myapp.communication.TokenResponse>
for method TokenApi.sendToken
at retrofit2.Utils.methodError(Utils.java:52)
To try and deal with this I have used moshi-kotlin-codegen to generate the proper adapter (hence the annotations in the data class), but to no avail. The class is generated, but not used. I have tried to pass a Moshi with JsonAdapterFactory like this var moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()to my ConverterFactory but that doesn't work either.
Tried to add the generated adapter maually to moshi but that also did not work.
I've also tried returning different types in my request. The Retrofit docs state that without a converter one could only return a ResponseBody, but same result: Retrofit complains it has no converter. The same for returning Call<Void>
I feel like I'm missing something here? Who can help? Happy to provide more details, please request what's needed.
Your request function should look like this.
#Headers( "Content-Type: application/json" )
#POST("/tokens?type=56427890283537921")
suspend fun sendFirebaseToken(#Body tokenRequest: RequestBody): TokenResponse
You don't use Call<...> since you have marked it as suspend.
Think the annotation should be:
#JsonClass(generateAdapter = true)
data class TokenResponse(
#field:Json(name = "errors") val errors: Integer,
#field:Json(name = "success") val success: Boolean
)
And try to remove the suspend keyword once, which might clash with generateAdapter = true.
I've got it working now, this is what I learned:
First of all: #Dominic Fischer here is right, Call is wrong, and with everything set up correctly, there is no need to wrap the result object at all (I noticed by the way the #Headers annotation looks to be not necessary, Retrofit seems to just take care of it).
The second and biggest problem is that the client object in my ApiClient class was not used correctly. See the new version:
fun getRetrofitService(): ApiService {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.client(getOkHttpClient())
.addConverterFactory(MoshiConverterFactory.create())
.build().create(ApiService::class.java)
}
See that now the 'create()' step is added, which before I handled outside of this class. There I used my Retrofit object to create the service just like here, but I accidentally passed ApiClient::class.java. Interestingly that compiles and runs just fine, but of course this must mess up somewhere - it's unable to properly build the JSON adapters.
As a result I pulled this step into my ApiClientin order to prevent such accidents in the future.
If anybody has suggestions as to meking this question + answer more useful for future readers, please let me know!
I've been wanting to adopt Dagger 2 in conjugation with Retrofit 2. All seems to work nicely except for GET requests; they doesn't seem to have any headers attached with them.
Below is my NetworkModule which provides all networking-related dependencies for the whole app (note the #ForApplication scope annotation sprinkled there):
#Module
public class NetworkModule {
// …
#ForApplication
#Provides
OkHttpClient provideOkHttp(
HttpLoggingInterceptor loggingInterceptor,
#Named(PREFERENCE_CUR_LOGIN_SESSION) Preference<LoginSession> loginSessionPreference,
DeviceCredentials deviceCredentials
) {
final OkHttpClient.Builder builder = new OkHttpClient().newBuilder();
builder.addNetworkInterceptor(chain -> {
if (loginSessionPreference.isSet()) {
return chain.proceed(
chain.request().newBuilder()
.addHeader("token", loginSessionPreference.get().getTokenId())
.addHeader("device-id", deviceCredentials.getDeviceId())
.addHeader("Content-Type", "application/json")
.build()
);
} else {
return chain.proceed(
chain.request().newBuilder().build()
);
}
});
return builder.build();
}
#ForApplication
#Provides
Retrofit provideRetrofit(Gson gson, OkHttpClient client) {
return new Retrofit.Builder()
.baseUrl("http://xxx.xxx.xxx/api/1.0/")
.client(client)
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
}
#ForApplication
#Provides
XxxApi provideApi(Retrofit retrofit) {
return retrofit.create(XxxApi.class);
}
}
This module is supplied as a dependency for my ApplicationComponent (among other modules):
#ForApplication
#Component(
modules = {
ApplicationModule.class,
RuntimeModule.class,
DateFormatModule.class,
PreferenceModule.class,
NetworkModule.class
}
)
public interface ApplicationComponent {
// …
}
I've ran a debug session and confirmed that loginSessionPreference.isSet() is evaluated as true but nevertheless my request still showed up without any headers:
11-16 16:55:22.748 21747-22569/xxx.xxx.xxx D/OkHttp: --> GET http://xxx.xxx.xxx/api/1.0/public/get-all-data/site http/1.1
11-16 16:55:22.748 21747-22569/xxx.xxx.xxx D/OkHttp: --> END GET
Did I miss something?
Use .addInterceptor instead of .addNetworkInterceptor()
First of all use addInterceptor() like Alex Shutov suggested.
Secondly, ensure in debug mode that methods addHeader() are called.
If you use the same Retrofit instance (same injection) it could not be used because loginSessionPreference.isSet() always returns false.
Your method provideOkHttp() is called while OkHttpClient is necessary to provide Retrofit instance. Method provideOkHttp() requires Preference and it is injected while object OkHttpClient is created. You can treat it as a final variable (compiler even makes it final I think).
Please delete loginSessionPreference logic and hardcode some headers - it will gonna tell us if it is the issue.
In my opinion you will need to change this architecture a little.
I'd like to be able to inject a new OkHttpClient using Dagger2 but am having a hard time. Most of the tutorials and instructions are written for applications and I'm building an aar (android library).
here's my Component:
#Singleton
#Component(modules = {NetworkModule.class})
public interface AndroidSdkComponent {
void inject(OkHttpClient httpClient);
}
here's my module:
#Module
public class NetworkModule {
#Provides
#Singleton
OkHttpClient provideOkHttpClient(Context context) {
final OkHttpClient client = new OkHttpClient();
if (Configuration.isAlphaBuild(context)) {
client.networkInterceptors().add(new StethoInterceptor());
}
return client;
}
}
At an entry point of my library I build the component like so:
DaggerAndroidSdkComponent.builder().networkModule(new NetworkModule())
.build();
but later when I'm trying to #Inject OkHttpClient okHttpClient it appears to always be null. What am I doing wrong?
What I'm I doing wrong?
void inject(OkHttpClient httpClient);
This will inject an OkHttpClient with the dependencies it needs...But since you neither call it, nor have access to the fields of the okhttp client anyways this method is redundant and useless.
DaggerAndroidSdkComponent.builder().networkModule(new NetworkModule())
.build();
This will build a component. It will build it. You still need to use it. e.g. call inject() and inject the object you want to inject.
What you need to do is update your interface to actually inject the object where you want the OkHttpClient in. You don't provide any information of where you want that client to be used, but you should have SomeClass like the following, where you already build the component with the code you provided above:
class SomeClass {
#Inject
OkHttpClient mClient(); // THIS is how you mark a field for injection
void someMethod() {
AndroidSdkComponent component = DaggerAndroidSdkComponent.builder().networkModule(new NetworkModule())
.build();
component.inject(this); // THIS is how you inject a field (okhttp!)
}
}
Then you need to update the component to know about SomeClass that you want to be injected:
#Singleton
#Component(modules = {NetworkModule.class})
public interface AndroidSdkComponent {
void inject(SomeClass someclass); // THIS will inject okhttp to your someclass object, as seen above
}
Please read some more tutorials about dagger and try playing with it, since it seems that you don't really understand how it works or what the different classes do. The User's Guide is a good place to start.