-->

Dagger2 one module for two different scopes

2019-06-06 07:25发布

问题:

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?

回答1:

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.