1

I have the following code working:

SomeClass

public class SomeClass {

    @Inject
    @Named("special")
    OkHttpClient mOkHttpClient;

    public SomeClass(Activity activity) {
        ((MyApplication) activity.getApplication()).getApplicationComponent().inject(this);
    }

}

ApplicationModule

@Module
public class ApplicationModule {

    private final Application mApplication;

    public ApplicationModule(Application application) {
        mApplication = Preconditions.checkNotNull(application);
    }

    @Provides
    @Singleton
    Application providesApplication() {
        return mApplication;
    }

    @Provides
    @Singleton
    SharedPreferences provideCustomSharedPreferences() {
        return mApplication.getSharedPreferences("my_custom_file", Context.MODE_PRIVATE);
    }

}

ApplicationComponent

@Singleton
@Component(modules = {
    ApplicationModule.class,
    NetworkModule.class
})
public interface ApplicationComponent {

    void inject(SomeClass someClass);

}

MainActivity

public class MainActivity extends AppCompatActivity {

    @Inject
    SharedPreferences mSharedPreferences;

    @Inject
    @Named("default")
    OkHttpClient mOkHttpClient;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        ((MyApplication) getApplication()).getActivityComponent().inject(this);
    }

}

NetworkModule

@Module
public abstract class NetworkModule {

    private static final int DEFAULT_CACHE_SIZE = 10 * 1024 * 1024; // 10 Mib

    @Provides
    @Singleton
    static Cache provideOkHttpCache(Application application) {
        return new Cache(application.getCacheDir(), DEFAULT_CACHE_SIZE);
    }

    @Provides
    @Singleton
    @Named("default")
    static OkHttpClient provideDefaultOkHttpClient(Cache cache) {
        OkHttpClient.Builder okHttpClient = new OkHttpClient.Builder();

        if (BuildConfig.DEBUG) {
            // Add logging interceptor here...
        }

        return okHttpClient.cache(cache).build();
    }

    @Provides
    @Singleton
    @Named("special")
    static OkHttpClient provideSpecialOkHttpClient(@Named("default") OkHttpClient okHttpClient) {
        return okHttpClient.newBuilder()
            // Add .certificatePinner() here...
            .build();
    }

}

ActivityComponent

@Singleton
@Component(modules = {
    ApplicationModule.class,
    NetworkModule.class
})
public interface ActivityComponent {

    void inject(MainActivity mainActivity);

}

MyApplication

public class MyApplication extends Application {

    private ApplicationComponent mApplicationComponent;
    private ActivityComponent mActivityComponent;

    @Override
    public void onCreate() {
        super.onCreate();
    }

    public ApplicationComponent getApplicationComponent() {
        if (mApplicationComponent == null) {
            mApplicationComponent = DaggerApplicationComponent.builder()
                .applicationModule(new ApplicationModule(this))
                .build();
        }

        return mApplicationComponent;
    }

    public ActivityComponent getActivityComponent() {
        if (mActivityComponent == null) {
            mActivityComponent = DaggerActivityComponent.builder()
                .applicationModule(new ApplicationModule(this))
                .build();
        }

        return mActivityComponent;
    }

}

But there are a few things that I don't like and I know they can be improved, with scopes, component dependencies or subcomponents. I can also use custom qualifiers instead of @Named, but I know that one already.

Here's what I want to do...

  1. I want the scope of provideSpecialOkHttpClient to be tied to SomeClass lifecycle. I know I need scopes for this... But this special OkHttpClient depends on the default OkHttpClient instance which itself depends on a okhttp3.Cache instance. These last two instances are @Singleton because they can be used everywhere. Only the special OkHttpClient instance exists tied to SomeClass because it's the only place where it will be used. How can I accomplish this with scopes? All my attempts led me to errors because I was using a custom scope, like @SomeClassScope, and @Singleton within the ApplicationComponent which itself is a @Singleton. In a nutshell, how can I have some singleton dependencies tied to the application lifecycle while having other dependencies tied to some other lifecycle (be it an Activity or SomeClass like in my example) when they are dependent of singleton dependencies?

  2. As you can see I'm calling new ApplicationModule(this) twice, defeating the purpose of @Singleton annotated components and provide methods. How can I make the ActivityComponent dependent on the ApplicationComponent so that I only have to instantiate ApplicationModule once in the ApplicationComponent? Subcomponents? Component dependencies? I couldn't make this work with any approach...

I'm having a hard time grasping all Dagger aspects so if you could provide some code examples when answering, that would really help me a lot visualize and understand how everything ties together.

rfgamaral
  • 16,546
  • 57
  • 163
  • 275

1 Answers1

3

First let's have a look at what you have so far. The ActivityComponent is a little bit strange. @Singleton represents app-scoped singleton. Since ActivityComponent injects members that have the scope of an Activity and not the entire app, we probably need a new scope for that Component like this:

@Retention(RetentionPolicy.RUNTIME)
@Scope
public @interface PerActivity {}

Now we can change that component:

@PerActivity
@Component(dependencies = { AppComponent.class 
})
public interface ActivityComponent {

    void inject(MainActivity mainActivity);

}

Notice we have now made it a dependent component of AppComponent. We will have to refactor AppComponent slightly to publish it's bindings to the dependent components.

Rationale: we want the ActivityComponent to be able to use the OkHttpClient that is bound in NetworkModule. However, NetworkModule is not a module of ActivityComponent - it is part of the parent AppComponent. Dependent components do not "automatically" inherit all of the bindings from their parents. In order for ActivityComponent to use the OkHttpClient as a dependency for the "special" OkHttpClient it needs to be exposed by the parent component. You can expose a binding to a dependent component by creating a method in the interface with the type you wish to expose. It's not necessary to expose all the bindings, just the very ones that you will use in the dependent components.

@Singleton
@Component(modules = {
    ApplicationModule.class,
    NetworkModule.class
})
public interface ApplicationComponent {

    //injection sites

    void inject(SomeClass someClass);

    //bindings published to subcomponents

    OkHttpClient okHttpClient(); //default

}

Now extract a module for the "special" OkHttpClient:

@Module
public class SpecialNetworkModule {

    @Provides
    @PerActivity
    @Named("special")
    static OkHttpClient provideSpecialOkHttpClient(@Named("default") OkHttpClient okHttpClient) {
        return okHttpClient.newBuilder()
        // Add .certificatePinner() here...
        .build();
    }
}

and compose the ActivityComponent with that same module:

@PerActivity
@Component(modules = { SpecialNetworkModule.class }
           dependencies = { AppComponent.class })
public interface ActivityComponent {

    void inject(MainActivity mainActivity);

}

SomeClass is basically in the scope of your activity, so you can refactor it to get injected inside your activity by doing this:

public class SomeClass {

    private final Activity activity;
    private final OkHttpClient mOkHttpClient;

    @Inject
    public SomeClass(Activity activity, @Named("special") OKHttpClient client) {
        this.activity = activity;
        this.mOkHttpClient = client;
    }
}

Now make SomeClass a field of MainActivity (I am assuming you are using it there because it has a dependency on Activity and it is the only Activity code you have provided):

@Inject SomeClass someClass

@Override
public void onCreate() {

And make sure your ActivityComponent provides Activity. To do this you will need a new Module:

@Module
@PerActivity
public class ActivityModule {

    private final Activity activity;

    public ActivityModule(Activity activity) {
        this.activity = activity;
    }

    @Provides
    @PerActivity
    Activity activity() {
        return this.activity;
    }

    @Provides
    @PerActivity
    Context context() {
         return this.activity;
    }
}

And compose your ActivityComponent with this module:

modules = { SpecialNetworkModule.class, ActivityModule.class }

Now the consumption of the components needs a little bit of work. Get rid of the public ActivityComponent getActivityComponent() inside your Application. ActivityComponents should be generated inside, well, an Activity in order for them to track the scopes and lifecycles correctly.

So consuming the component inside your Activity should look something like this:

@Inject SomeClass someClass;

@Override
public void onCreate() {
     AppComponent appComponent = ((((MyApplication) getApplication()).getActivityComponent());
     DaggerActivityComponent.builder()
         .appComponent(appComponent)
         .activityModule(new ActivityModule(this))
         .build()
         .inject(this);
}

Finally, to answer your two questions explicitly:

In a nutshell, how can I have some singleton dependencies tied to the application lifecycle while having other dependencies tied to some other lifecycle (be it an Activity or SomeClass like in my example) when they are dependent of singleton dependencies?

By creating a custom scope (@PerActivity), a component that tracks that scope (ActivityComponent), and using separate modules (SpecialNetworkModule, ActivityModule) for the narrower scoped dependencies. In doing this, you will need some form of relationship between the wider-scoped and narrower-scoped components. Which leads well to your next question:

How can I make the ActivityComponent dependent on the ApplicationComponent so that I only have to instantiate ApplicationModule once in the ApplicationComponent? Subcomponents? Component dependencies? I couldn't make this work with any approach...

As in the above example, using dependent components (subcomponents are also a possibility to consider). In doing this, make sure that wider-scoped components explicitly publish their bindings to their dependent components.

David Rawson
  • 20,912
  • 7
  • 88
  • 124
  • I've started to refactor my whole Dagger code (by restoring everything to pre-Dagger and starting over) and I've used many approaches you described above (been reading a lot of SO posts on the subject after posting this question). The only difference is that I'm using subcomponents instead, looks cleaner to me. I have not yet created the special module for just that single `OkHttpClient`, but that makes a lot of sense. Will try that soon. Thanks. – rfgamaral Jan 04 '17 at 09:54
  • I'm just confused about the bindings thing... You are exposing methods for `Application`, `SharedPreferences`, `OkHttpClient` and `Cache` but I don't understand **where** and **when** they are used? Could you edit your answer to clarify that please. – rfgamaral Jan 04 '17 at 09:54
  • @Ricardo Apologies - I can see how what I wrote previously was confusing. In the use case for your post, I can only see the need to publish `OkHttpClient` as it is used as a dependency in the `SpecialNetworkModule`. Publishing `Application` and `SharedPreferences` was just in case you wanted to use those in other dependent components in the future. A lot of what I have written is probably not applicable for you now because you are using sub-components. But the essence is the same- you need some kind of hierarchy of components with narrowing of scope. Perhaps you would like to self-answer? – David Rawson Jan 04 '17 at 10:17
  • I think your answer is good, I'll just need to refactor my code with what you said in mind and see if solves my initial issues (which I think it does). I'm still confused about the binding... Let me put my question this way: If I remove `OkHttpClient okHttpClient();` from `ApplicationComponent`, wouldn't everything work anyway? If not, why not? – rfgamaral Jan 04 '17 at 12:10
  • @Ricardo I am finding that adding more to that part of the answer is going to break its flow. To answer your question here, if you remove the method `okHttpClient()` from `AppComponent` then the code would not compile. Why? Because `ActivityComponent` could not resolve all of its dependencies. Why? Because `ActivityComponent` uses `SpecialNetworkModule` that provides special OkHttpClient that has a dependency on the default OkHttpClient. I am happy to provide more explanation if necessary but it would better suit a new question – David Rawson Jan 04 '17 at 19:00
  • I don't mind creating a new question but now I don't know exactly what to ask since you are saying the code won't compile but I just got it to compile following more or less your answer :/ – rfgamaral Jan 04 '17 at 19:16
  • I'll ask the question if you like and post a link here. One more thing (I couldn't edit the above comment). "Doesn't compile" won't apply if you aren't actually injecting OkHttpClient. So if the injection sites for your component don't contain any `@Inject` annotations for dependencies that actually need the "special" `OkHttpClient` it will still let you compile. If this answer has been helpful, please accept and upvote. At least 60% of my answers go unacknowledged. – David Rawson Jan 04 '17 at 19:20
  • Well, I'm actually using using constructor injection but I just tried to remove the parameter from the constructor and use `@Inject` instead. They both compiled just fine. Don't worry, I always upvote/accept my questions :) – rfgamaral Jan 04 '17 at 19:25
  • @Ricardo I added a question [here](https://stackoverflow.com/questions/41472319/what-is-the-purpose-of-the-non-inject-methods-in-components-in-dagger-2/41472320#41472320) I hope it explains this point – David Rawson Jan 04 '17 at 20:03