35

I'm wondering if there is a way for Dagger to know that it should recreate an object when new data is available.

The instance I am speaking of is with the request headers I have for retrofit. At some point (when the user logs in) I get a token that I need to add to the headers of retrofit to make authenticated requests. The issue is, I'm left with the same unauthenticated version of retrofit. Here's my injection code:

@Provides
    @Singleton
    OkHttpClient provideOkHttpClient(Cache cache) {
        HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
        interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
        OkHttpClient client = new OkHttpClient.Builder()
                .addInterceptor(interceptor)
                .cache(cache).build();
         client
                .newBuilder()
                .addInterceptor(
                    chain -> {
                        Request original = chain.request();
                        Request.Builder requestBuilder = original.newBuilder()
                                .addHeader("Accept", "Application/JSON");
                        Request request = requestBuilder.build();
                        return chain.proceed(request);
                    }).build();
        return client;
    }

  @Provides
    @Singleton
    Retrofit provideRetrofit(Gson gson, OkHttpClient okHttpClient) { 
        Retrofit retrofit = new Retrofit.Builder()
                .addConverterFactory(GsonConverterFactory.create(gson))
                .addCallAdapterFactory(RxErrorHandlingCallAdapterFactory.create())
                .baseUrl(mBaseUrl)
                .client(okHttpClient)
                .build();
        return retrofit;
}

@Provides
    @Singleton
    public NetworkService providesNetworkService(Retrofit retrofit) {
        return retrofit.create(NetworkService.class);
    }

Any ideas on how to make this work?

azizbekian
  • 60,783
  • 13
  • 169
  • 249
AIntel
  • 1,087
  • 5
  • 14
  • 27

4 Answers4

59

I personally created an okhttp3.Interceptor that does that for me, which I update once I have the required token. It looks something like:

@Singleton
public class MyServiceInterceptor implements Interceptor {
  private String sessionToken;

  @Inject public MyServiceInterceptor() {
  }

  public void setSessionToken(String sessionToken) {
    this.sessionToken = sessionToken;
  }

  @Override public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();

    Request.Builder requestBuilder = request.newBuilder();

    if (request.header(NO_AUTH_HEADER_KEY) == null) {
      // needs credentials
      if (sessionToken == null) {
        throw new RuntimeException("Session token should be defined for auth apis");
      } else {
        requestBuilder.addHeader("Cookie", sessionToken);
      }
    }

    return chain.proceed(requestBuilder.build());
  }
}

In the corresponding dagger component, I expose this interceptor so I can set the sessionToken when I need to.

That is some stuff that Jake talked about it his talk Making Retrofit Work For You.

oldergod
  • 15,033
  • 7
  • 62
  • 88
  • 1
    Is there an example of this. – Rachit Mishra Dec 27 '17 at 09:07
  • 1
    @RachitMishra You want to expose the interceptor to the parts of your app which would update your token, and use this same instance of the interceptor in the Retrofit instance you provide to your app. That way you can update the sessionToken for the entire app from one point in the app. – urgentx Mar 21 '18 at 21:57
  • @RachitMishra can you please provide a completed example? I don't know how to use the `MyServiceInterceptor` instance into my `Module` which needs Interceptor – matio Apr 05 '18 at 17:42
  • @matio the interceptor is already injectable. so in the function, from where you provide Okhttp include this as a parameter. and add this as a interceptor to the OkHttp instance. – Rachit Mishra Apr 05 '18 at 17:45
  • @RachitMishra but in the `component` where I want to `expose` it, it is unused still and gives me null exception. How would we expose without `@Provide` annotation? – matio Apr 06 '18 at 12:01
  • Hi,I'm using dagger2 + Retrofit & Okhttp & need to add interceptor to the okhttp.I have done as per the @oldergod code & tried to configure dagger2 but i guess i'm missing something,can any one post some example on this in detail. – Pranesh Sahu Nov 13 '18 at 12:26
  • that is how I solve it, in addition to @urgentx answer: mark your `TokenInterceptor` with `@Singleton` (so we have one TokenInterceptor per application), then inject TokenInterceptor to your `Repository` ( that repository talks to Retrofit) - just like you injecting `ApiService` or `DAOs` in `Repository`. Once you obtain token in Repository level - just set `tokenInterceptor.token` property - that's it! – Roman Jun 24 '19 at 12:31
  • Since `intercept(Chain chain)` is often called in the background, shouldn't `sessionToken` be marked as `volatile`? – ivan8m8 May 30 '22 at 10:31
  • Hi, nice solution. @urgentx I was implementing this in Kotlin, with Dagger2 (not DaggerHilt) and had some problem with the persistence of the token in the class `MyServiceInterceptor`, it is not being held in the Singleton instance (set with `setSessionToken`), so it's never appended to the requests. I had to create a static var (`@JvmStatic object` for kotlin), and I don't think is a clean solution. Any idea on why it is not persisting the token? – Vento Jan 27 '23 at 18:28
12

Please consider using the approach mentioned by @oldergod as it is the "official" and much better way, whereas the approaches mentioned below are not advised, they may be considered as workarounds.


You have a couple of options.

  1. As soon as you get the token, you have to null out the component that provided you the Retrofit instance, create a new component and ask for a new Retrofit instance, which will be instantiated with necessary okhttp instance.
  2. A fast and bad one - Save the token in SharedPreferences, create okHttp header, which will apply token reading from SharedPreferences. If there is none - send no token header.
  3. Even uglier solution - declare a static volatile String field, and do the same thing like in step 2.

Why the second option is bad? Because on each request you would be polling disk and fetch data from there.

azizbekian
  • 60,783
  • 13
  • 169
  • 249
  • For option 1. How do I ask for a new retrofit instance? – AIntel Mar 27 '17 at 17:23
  • Assuming you have a `FooComponent fooCompomnent`, that provides `Retrofit` instance. Now you `null` out that component and create a new `FooComponent`. – azizbekian Mar 27 '17 at 17:25
  • Marking your answer as correct, since I understand and see how your solution would work. The problem with my setup is that I constructor inject retrofit to my presenter which itself is injected in my view. And I do this across my views some of which have their own components which depend on my network component. I was hoping for some magic solution that would somehow force my presenters to be reinjected across all of my views. – AIntel Mar 27 '17 at 18:43
  • 1
    No need to null out the Component, because you can set the token into the interceptor just after the successful login / save into database of the logged user which doesn't need the token. @oldergod 's answer should be marked as more correct. The Jake's talk is very interesting too. – Davideas Sep 24 '17 at 20:25
  • @Davidea, totally agree. At the time of writing this I hadn't seen Jake's talk. – azizbekian Sep 25 '17 at 05:57
  • `SharedPreferences` implementation [has in-memory caching](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/SharedPreferencesImpl.java), it won't read from disk on each access. I suppose recreating Retrofit & OkHttp causes much more overhead than other options. – gmk57 Jun 28 '22 at 08:05
1

Created custom RequestInterceptor with @Inject constructor

RequestInterceptor

@Singleton
class
RequestInterceptor @Inject constructor(
    private val preferencesHelper: PreferencesHelper,
) : Interceptor {

    @Throws(IOException::class)
    override fun intercept(chain: Interceptor.Chain): Response {
        var newRequest: Request = chain.request()

        newRequest = newRequest.newBuilder()
            .addHeader(
                "AccessToken",
                preferencesHelper.getAccessTokenFromPreference()
            )
            .build()


        Log.d(
            "OkHttp", String.format(
                "--> Sending request %s on %s%n%s",
                newRequest.url(),
                chain.connection(),
                newRequest.headers()
            )
        );
        return chain.proceed(newRequest)

  }

ApplicationModule

@Module(includes = [AppUtilityModule::class])
class ApplicationModule(private val application: AppController) {

    @Provides
    @Singleton
    fun provideApplicationContext(): Context = application

    @Singleton
    @Provides
    fun provideSharedPreferences(): SharedPreferences =
        PreferenceManager.getDefaultSharedPreferences(application.applicationContext)

}

PreferencesHelper

@Singleton
class PreferencesHelper
@Inject constructor(
    private val context: Context,
    private val sharedPreferences: SharedPreferences
) {
    private val PREF_KEY_ACCESS_TOKEN = "PREF_KEY_ACCESS_TOKEN"


    fun getAccessTokenFromPreference(): String? {
        return sharedPreferences.getString(PREF_KEY_ACCESS_TOKEN, null)
    }

}
Naveen Dew
  • 1,295
  • 11
  • 19
0

Well tested and working

public OkHttpClient getHttpClient(Context context) {
    HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
    logging.setLevel(HttpLoggingInterceptor.Level.BODY);
    return  new OkHttpClient.Builder()
            .connectTimeout(10, TimeUnit.SECONDS)
            .callTimeout(60,TimeUnit.SECONDS)
            .writeTimeout(60, TimeUnit.SECONDS)
            .readTimeout(60, TimeUnit.SECONDS)
            .addInterceptor(logging)
            .addInterceptor(chain -> {
                Request newRequest = chain.request().newBuilder()
                        .addHeader("Authorization", "Bearer " + Utility.getSharedPreferencesString(context, API.AUTHORIZATION))
                        .build();
                return chain.proceed(newRequest);
            })

            .build();

}

Earlier I was wondering, if session expires and user login again, will this interceptor replace the existing auth, but fortunately it is working fine.

Ankit Dubey
  • 1,000
  • 1
  • 8
  • 12