1

In my app i have an ApplicationScope which provides me with stuff like context and shared prefs. 2 sub components, LoggedInComponent and LoggedOutComponent. Those 2 subcomponents have other subcomponents which are less relevant to the issue.

I have a NetworkModule which creates my retrofit instance. Both subcomponents need the NetworkModule but the retrofit might change during the login because the base url and other settings might change.

I was wondering which approach it is better to take: 1. Give both LoggedIn and LoggedOut sub components and the NetworkModule the same scope so both sub components can use this module. Feels a bit hacky and wrong usage of scopes. 2. Put network module in the AppComponent with ApplicationScope. Also wrong usage of scope because network module is recreated so it cannot have same scope and also each recreation will cause the AppComponent and its subcomponents to be recreated.

What would you do? maybe there is a better solution?

danidin
  • 371
  • 2
  • 17

1 Answers1

3

You don't want LoggedInComponent and LoggedOutComponent to have differing network behavior, and you don't necessarily need network behavior to be consistent across the lifetime of LoggedInComponent or LoggedOutComponent, so it doesn't make sense for you to have them bind NetworkModule separately. I think it makes sense to make them available through AppComponent. Remember that not everything in AppComponent needs to have ApplicationScope: You can make unscoped bindings in AppComponent, which instructs Dagger to re-fetch or re-create the requested object every time.

Specifically, I would bind NetworkModule into a subcomponent of AppComponent, which you can recreate every time the NetworkModule configuration changes. Not only would this let you encapsulate some of the network details (see "subcomponents for encapsulation" in the Dagger User's Guide), but it would also let you take advantage of dependency injection throughout your network bindings if that's what you're looking for.

To ensure that you're getting the correct current NetworkComponent, you could create a NetworkManager that is singleton-bound (ApplicationScope). You can use that to hold the latest NetworkComponent.

@Module(subcomponents={NetworkComponent.class})
public abstract class ApplicationModule {
  /** Unscoped. Fetch this fresh from NetworkComponentHolder each time. */
  @Provides Retrofit provideRetrofit(NetworkManager manager) {
    return manager.getNetworkComponent().getRetrofit();
  }
}

@ApplicationScope public class NetworkManager {
  private final Provider<NetworkComponent.Builder> builderProvider;
  private NetworkComponent currentComponent;

  @Inject public NetworkComponentHolder(
      Provider<NetworkComponent.Builder> builderProvider) {
    this.builderProvider = builderProvider;
    currentComponent = builderProvider.get()
        .withNetworkModule(getDefault())
        .build();
  }

  public void updateSettings(String baseUrl) {
    currentComponent = builderProvider.get()
        .withNetworkModule(new NetworkModule(baseUrl))
        .build();
  }

  public NetworkComponent getNetworkComponent() {
    return currentComponent;
  }
}

With this, most of your code in AppComponent, LoggedInComponent, and LoggedOutComponent can just inject a Retrofit (or Provider<Retrofit>) whenever they need to make a request. When a response comes back that tells you to update your base URL, you can inject a NetworkManager, call updateSettings, and suddenly new requests for Retrofit will return your new instance. (Note however that old instances to Retrofit may still stick around, but you'll have that problem any time you're changing a dependency belonging to an existing instance.)

p.s. If NetworkModule is lightweight enough, or has consistent-enough bindings, you might opt to put NetworkModule directly onto ApplicationComponent and simply have NetworkManager hold the current Retrofit instance etc. You'll have to make that judgment call based on the number of bindings you want to pass through as provideRetrofit does, compared to the number of bindings you'd want to encapsulate or hide away in a subcomponent.

Jeff Bowman
  • 90,959
  • 16
  • 217
  • 251
  • Why did you decide to make networkComponent a subcomponent of ApplicationModule and not subcomponent of ApplicationComponent? Also, i guess you meant NetworkManager and not NetworkComponentHolder, right? – danidin Mar 31 '17 at 08:18
  • Yes, I'd switched between NetworkManager and NetworkComponentHolder, sorry; use whichever name makes more sense to you. The `subcomponents={}` list is intentionally on the Module, though; if you don't need it exposed to public consumers of AppComponent as a builder or `plus` method, you can list it on a Module and Dagger will simply make the Builder injectable among Module's component's bindings. – Jeff Bowman Mar 31 '17 at 11:13
  • Couple of things i don't quite understand: 1. who holds NetworkManager? Is it being hold by NetworkComponent because it uses it? 2. Can you write down NetworkModule and NetworkComponent declaratios please? and also the ApplicationComponent 3. i still dont understand why NetworkComponent is subcomponent of a module and not of the component... Thanks :) – danidin Apr 01 '17 at 14:44
  • Also, please correct me if im wrong, but in this way, NetworkComponent will be LoggedInComponents brother, so if my every activity injected through its ActivitySubcomponent, which is a subcomponent of LoggedInSubcomponent, I wont get retrofit automatically and will need to inject it with another call... – danidin Apr 02 '17 at 03:29
  • In my suggestion, ApplicationComponent binds NetworkManager, and NetworkManager holds the current NetworkComponent. This allows you to get and update the current NetworkComponent at any time; you could even write a `@Provides` method that delegates to NetworkManager's current NetworkComponent instance so you can inject Retrofit anywhere in your app. Regarding subcomponents, Components have two ways to include a subcomponent: On the Component interface as a public method, or on a Module that the Component includes—which doesn't need to be publicly exposed on the Component. I picked the latter. – Jeff Bowman Apr 02 '17 at 04:56
  • Could you please write how NetworkComponent should look? Thanks! – danidin Apr 02 '17 at 07:02
  • If i want protocol to be injected via constructor injection, i need AppComponent to have a line like: Retrofit getRetrofit and in AppModule i need a @Provides method but then, in compile time i get an error: "Retrofit is bound multiple times" since two methods provide it..the original in NetworkModule and the new one in AppModule for the Ctor injection – danidin Apr 02 '17 at 09:02
  • Yes; you might need a qualifier annotation to distinguish them. You might also just switch off to letting the Module handle it inline, [as in this striking similar question here](http://stackoverflow.com/a/43147736/1426891). – Jeff Bowman Apr 02 '17 at 14:44