How Dagger Scoping works?

Abhishek Luthra
8 min readNov 13, 2020

--

When I learned how to use Dagger, the concept of so-called scopes was one of the most difficult to grasp.

Today I realize that the best way to understand Dagger scopes is to dive into the depths of the generated code. It sounds complicated, but I assure you that it is not. Well, it is a little complicated if you do it on your own, but you don’t have to.

In this post, I will take you through the code that Dagger generates and explain the inner workings of scopes.

Dagger 2 dependency injection framework:

I would like to make sure that the terminology I’m going to use is clear, so let’s briefly review Dagger’s constructs. If you’re completely new to Dagger, you might want to start with a proper Dagger tutorial and get back to this post afterward.

Dagger 2 is a dependency injection framework for Android and Java. Originally created by Square, it is now being maintained by Google.

The basic “building blocks” that Dagger 2 uses are as follows:

  • Components. Components are “injectors” that perform actual injection into “clients”.
  • Modules. Modules are the objects in which “object graph” structure is defined (the way “services” need to be instantiated).
  • Scopes. Scopes are Java annotations which, when used, change the way “services” are being instantiated upon injection.

Please note the terminology: in this post, whenever I say “service” I’m not referring to the android Service class, but to some object which is being injected into another object; whenever I say “client” I’m referring to some object that is being injected with another object.

Injection without scopes:

To better understand what scopes do, let’s review what happens when I inject “un-scoped services”.

For simplicity, I’m going to inject this class:

public class UnScopedService {
public UnScopedService() {
}
}

UnScopedService is being declared as a service (i.e. being declared as “injectable”) in this module:

@Module
public class DemoModule {@Provides
public UnScopedService provideUnscopedService() {
return new UnScopedService();
}}

This is the component that makes use of DemoModule and declares MainActivity as a client (i.e. declares that it can inject services into MainActivity):

@Component(modules = {DemoModule.class})
public interface DemoComponent {
void inject(MainActivity mainActivity);
}

And the client is also very simple (the contents of activity_main.xml are irrelevant to our current discussion):

public class MainActivity extends AppCompatActivity {

@Inject
UnScopedService mUnScopedService;
private DemoComponent mDemoComponent;
@Override
public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) {
getDemoComponent().inject(this);
super.onCreate(savedInstanceState, persistentState);
setContentView(R.layout.activity_main);
}
private DemoComponent getDemoComponent() {
if (mDemoComponent == null) {
mDemoComponent = DaggerDemoComponent.builder().demoModule(new DemoModule()).build();
}
return mDemoComponent;
}
}

After building this project I can take a look at the actual code which performs the injection.

Starting with the inject(MainActivity) method of DemoComponent, I trace the usages and implementations of a series of classes and eventually reach MainActivity_MembersInjector class:

public final class MainActivity_MembersInjector implements MembersInjector<MainActivity> {    private final Provider<UnScopedService> mUnScopedServiceProvider;    public MainActivity_MembersInjector(Provider<UnScopedService> mUnScopedServiceProvider) {
assert mUnScopedServiceProvider != null;
this.mUnScopedServiceProvider = mUnScopedServiceProvider;
}
public static MembersInjector<MainActivity> create(
Provider<UnScopedService> mUnScopedServiceProvider) {
return new MainActivity_MembersInjector(mUnScopedServiceProvider);
}
@Override
public void injectMembers(MainActivity instance) {
if (instance == null) {
throw new NullPointerException("Cannot inject members into a null reference");
}
instance.mUnScopedService = mUnScopedServiceProvider.get();
}
public static void injectMUnScopedService(
MainActivity instance, Provider<UnScopedService> mUnScopedServiceProvider) {
instance.mUnScopedService = mUnScopedServiceProvider.get();
}
}

The last Line in injectMembers method is where the actual injection of UnScopedService takes place. Note that the service is not constructed here, but is retrieved from an instance of Provider interface.

To find out which implementation of Provider was used, I search for usages of MainActivity_MembersInjector#create() method. It is being called from DaggerDemoComponent:

public final class DaggerDemoComponent implements DemoComponent {
private Provider<UnScopedService> provideUnscopedServiceProvider;
private MembersInjector<MainActivity> mainActivityMembersInjector; private DaggerDemoComponent(Builder builder) {
assert builder != null;
initialize(builder);
}
public static Builder builder() {
return new Builder();
}
public static DemoComponent create() {
return new Builder().build();
}
@SuppressWarnings("unchecked")
private void initialize(final Builder builder) {
this.provideUnscopedServiceProvider =
DemoModule_ProvideUnscopedServiceFactory.create(builder.demoModule);
this.mainActivityMembersInjector =
MainActivity_MembersInjector.create(provideUnscopedServiceProvider);
}
@Override
public void inject(MainActivity mainActivity) {
mainActivityMembersInjector.injectMembers(mainActivity);
}
public static final class Builder {
private DemoModule demoModule;
private Builder() {} public DemoComponent build() {
if (demoModule == null) {
this.demoModule = new DemoModule();
}
return new DaggerDemoComponent(this);
}
public Builder demoModule(DemoModule demoModule) {
this.demoModule = Preconditions.checkNotNull(demoModule);
return this;
}
}
}

As you can see in the initialize method, DemoModule_ProvideUnscopedServiceFactory is being used as a Provider for instances of UnScopedService.

Let’s see what’s going on inside this class:

public final class DemoModule_ProvideUnscopedServiceFactory implements Factory<UnScopedService> {
private final DemoModule module;
public DemoModule_ProvideUnscopedServiceFactory(DemoModule module) {
assert module != null;
this.module = module;
}
@Override
public UnScopedService get() {
return Preconditions.checkNotNull(
module.provideUnscopedService(),
"Cannot return null from a non-@Nullable @Provides method");
}
public static Factory<UnScopedService> create(DemoModule module) {
return new DemoModule_ProvideUnscopedServiceFactory(module);
}
}

The code in the last line in getting method simply obtains an instance of UnScopedService from DemoModule, which is the class that I myself defined. Looks like when a non-scoped service needs to be injected, the code that Dagger 2 generates simply delegates the instantiation of that service to a method in the user-defined Module class.

Imagine that I would like to use the same instance of DemoComponent to inject UnScopedService into multiple fields. Since each injection into a field results in a new instance being retrieved from the module, each field will point to a different service.

Remember this point because, as we’ll see shortly, it is exactly this mechanism of instantiation of a new service for each field that scopes will affect.

Injection with @Singleton scope:

Dagger 2 recognizes a single predefined scope: @Singleton. Let me show you how to apply scope to service.

Then I’ll dig into what happens when Dagger injects service annotated with @Singleton scope.

The scoped service will also be very simple:

public class SingletonScopedService {    public SingletonScopedService() {
}

}

Please note that the service itself has no dependencies on the Dagger 2 framework, and, therefore, is not aware of its scope. I include the scope’s name in the service’s name just for a purpose of this tutorial, but you should not do the same in your projects.

In order to assign a scope to the service, we add @Singleton annotation to its provider method in DemoModule class:

@Module
public class DemoModule {
@Provides
public UnScopedService provideUnscopedService() {
return new UnScopedService();
}
@Provides
@Singleton
public SingletonScopedService singletonScopedService() {
return new SingletonScopedService();
}
}

Dagger 2 enforces a rule that in order to inject scoped services, the injecting component should also be annotated with the corresponding scope:

@Singleton
@Component(modules = {DemoModule.class})
public interface DemoComponent {
void inject(MainActivity mainActivity);
}

And the client that uses both services becomes:

public class MainActivity extends AppCompatActivity {    @Inject
UnScopedService mUnScopedService;
@Inject
SingletonScopedService mSingletonScopedService;
private DemoComponent mDemoComponent; @Override
public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) {
getDemoComponent().inject(this);
super.onCreate(savedInstanceState, persistentState);
setContentView(R.layout.activity_main);
}
private DemoComponent getDemoComponent() {
if (mDemoComponent == null) {
mDemoComponent = DaggerDemoComponent.builder().demoModule(new DemoModule()).build();
}
return mDemoComponent;
}
}

Now it is time to trace the methods and classes involved in the injection of SingletonScopedService in exactly the same manner as I did for UnScopedService beforehand.

When I do this I find out that the only difference in treatment of these services is in DaggerDemoComponent class:

public final class DaggerDemoComponent implements DemoComponent {
private Provider<UnScopedService> provideUnscopedServiceProvider;
private Provider<SingletonScopedService> singletonScopedServiceProvider; private MembersInjector<MainActivity> mainActivityMembersInjector; private DaggerDemoComponent(Builder builder) {
assert builder != null;
initialize(builder);
}
public static Builder builder() {
return new Builder();
}
public static DemoComponent create() {
return new Builder().build();
}
@SuppressWarnings("unchecked")
private void initialize(final Builder builder) {
this.provideUnscopedServiceProvider =
DemoModule_ProvideUnscopedServiceFactory.create(builder.demoModule);
this.singletonScopedServiceProvider =
DoubleCheck.provider(DemoModule_SingletonScopedServiceFactory.create(builder.demoModule));
this.mainActivityMembersInjector =
MainActivity_MembersInjector.create(
provideUnscopedServiceProvider, singletonScopedServiceProvider);
}
@Override
public void inject(MainActivity mainActivity) {
mainActivityMembersInjector.injectMembers(mainActivity);
}
public static final class Builder {
private DemoModule demoModule;
private Builder() {} public DemoComponent build() {
if (demoModule == null) {
this.demoModule = new DemoModule();
}
return new DaggerDemoComponent(this);
}
public Builder demoModule(DemoModule demoModule) {
this.demoModule = Preconditions.checkNotNull(demoModule);
return this;
}
}
}

Take a close look at the code in the initialize method. While DemoModule_ProvideUnscopedServiceFactory is directly used in MainActivity_MembersInjector, DemoModule_SingletonScopedServiceFactory is wrapped by DoubleCheck(which is another implementation of Provider interface).

Since this is the only difference between injection mechanisms of UnScopedService and SignletonScopedService by Dagger, then whatever functional differences @Singleton scope introduces, all of them will be due to Decoration by DoubleCheck class.

public final class DoubleCheck<T> implements Provider<T>, Lazy<T> {
private static final Object UNINITIALIZED = new Object();
private volatile Provider<T> provider;
private volatile Object instance = UNINITIALIZED;
private DoubleCheck(Provider<T> provider) {
assert provider != null;
this.provider = provider;
}
@SuppressWarnings("unchecked") // cast only happens when result comes from the provider
@Override
public T get() {
Object result = instance;
if (result == UNINITIALIZED) {
synchronized (this) {
result = instance;
if (result == UNINITIALIZED) {
result = provider.get();
Object currentInstance = instance;
if (currentInstance != UNINITIALIZED && currentInstance != result) {
throw new IllegalStateException("Scoped provider was invoked recursively returning "
+ "different results: " + currentInstance + " & " + result + ". This is likely "
+ "due to a circular dependency.");
}
instance = result;
provider = null;
}
}
}
return (T) result;
}
public static <T> Provider<T> provider(Provider<T> delegate) {
checkNotNull(delegate);
if (delegate instanceof DoubleCheck) {
return delegate;
}
return new DoubleCheck<T>(delegate);
}
public static <T> Lazy<T> lazy(Provider<T> provider) {
if (provider instanceof Lazy) {
@SuppressWarnings("unchecked") final Lazy<T> lazy = (Lazy<T>) provider;
return lazy;
}
return new DoubleCheck<T>(checkNotNull(provider));
}
}

Even though this code looks complicated, it is the standard approach to ensuring that only one instance of service will be returned. This class caches the service returned by the first call to get() method, and then returns the same service on subsequent calls.

This makes DoubleCheck a Caching Decorator — it is a provider that wraps around another provider and caches the instance returned by it.

It is worth noting that the implementation of get() method in DoubleCheck makes use of a thread-safe “double check” idiom. This might be important if you intend to use DemoComponent from multiple threads. However, in practice, I have never seen a reason to perform injection on a non-UI thread in Android.

What would happen if I’d use the same instance of DemoComponent to inject SingletonScopedService into multiple fields?

Well, since the first injected service is being cached, all fields would end up having a reference to the same object.

This outcome is different from what would happen with fields of type UnScopedService (each of which would point to a different service) and this is the only difference introduced by @Singleton scope annotation.

Injection with user-defined scope:

In addition to a predefined @Singleton scope, I can define my own scopes in the following manner (this code should be in a file named CustomScope.java):

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

Once the scope is defined in this manner, I can use it in my project. For example, I could replace all occurrences of @Singleton annotation with @CustomScope annotation.

If I trace the auto-generated code just like before, I’ll find out that Dagger treats @CustomScope in exactly the same way as it treats @Singleton scope. This means that “all scopes built equal” and the names of the scopes are irrelevant from Dagger’s perspective (though I do encourage you to name them descriptively for readability).

Conclusion:

  • Any time an un-scoped service is being injected by the same component, a new instance of a service is created.
  • The first time a @Singleton scoped service is being injected, it is instantiated and cached inside the injecting component, and then the same exact instance will be used upon injection into other fields of the same type by the same component.
  • Custom user-defined scopes are functionally equivalent to a predefined @Singleton Scope
  • Injection of scoped services is thread-safe.

That’s what scopes actually do.

Since components cache and reuse the same instance of scoped services upon injection (irrespective of scope’s name), the question of “whether the injected service is singleton or not” can be reduced to a question of “whether instances of a service were injected with the same component”.

--

--