Dagger 2 Scopes Demystified

/, Java development/Dagger 2 Scopes Demystified

Among all Dagger features that I use, the concept of so-called scopes was one of the most difficult to grasp. I finally understood how scopes work only after deep diving into the Dagger generated code.

Today I realize that the best way to learn about Dagger scopes is to simply start with such a review. It sounds complicated, but I assure you that it is not. Well, it is complicated a little bit 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 the basic constructs of Dagger dependency injection framework. If you’re completely new to Dagger, you might want to start with a proper Dagger tutorial and get back to this post afterwards.

The basic “building blocks” that Dagger 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 client-service terminology that I will use in this post.

Whenever I say “service” I’m not referring to android Service class, but to some object which is being injected into other object. Whenever I say “client” I’m referring to some object that is being injected with one or more services.

By the way, you can grab the source code that I’m going to use from this GitHub repository.

Injection without scopes:

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

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

public class NonScopedService {

    public NonScopedService() {}

}

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

@Module
public class TutorialModule {

    @Provides
    public NonScopedService nonScopedService() {
        return new NonScopedService();
    }

}

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

@Component(modules = {TutorialModule.class})
public interface TutorialComponent {

    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 NonScopedService mNonScopedService;

    private TutorialComponent mTutorialComponent;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        getTutorialComponent().inject(this);

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);
    }

    private TutorialComponent getTutorialComponent() {
        if (mTutorialComponent == null) {
            mTutorialComponent = DaggerTutorialComponent.builder()
                    .tutorialModule(new TutorialModule())
                    .build();
        }
        return mTutorialComponent;
    }
}

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

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

@Generated("dagger.internal.codegen.ComponentProcessor")
public final class MainActivity_MembersInjector implements MembersInjector<MainActivity> {
  private final MembersInjector<AppCompatActivity> supertypeInjector;
  private final Provider<NonScopedService> mNonScopedServiceProvider;

  public MainActivity_MembersInjector(MembersInjector<AppCompatActivity> supertypeInjector, Provider<NonScopedService> mNonScopedServiceProvider) {  
    assert supertypeInjector != null;
    this.supertypeInjector = supertypeInjector;
    assert mNonScopedServiceProvider != null;
    this.mNonScopedServiceProvider = mNonScopedServiceProvider;
  }

  @Override
  public void injectMembers(MainActivity instance) {  
    if (instance == null) {
      throw new NullPointerException("Cannot inject members into a null reference");
    }
    supertypeInjector.injectMembers(instance);
    instance.mNonScopedService = mNonScopedServiceProvider.get();
  }

  public static MembersInjector<MainActivity> create(MembersInjector<AppCompatActivity> supertypeInjector, Provider<NonScopedService> mNonScopedServiceProvider) {  
      return new MainActivity_MembersInjector(supertypeInjector, mNonScopedServiceProvider);
  }
}

Line 19 is where the actual injection of NonScopedService takes place. Note that the service is not constructed here, but being 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 DaggerTutorialComponent:

@Generated("dagger.internal.codegen.ComponentProcessor")
public final class DaggerTutorialComponent implements TutorialComponent {
  private Provider<NonScopedService> nonScopedServiceProvider;
  private MembersInjector<MainActivity> mainActivityMembersInjector;

  private DaggerTutorialComponent(Builder builder) {  
    assert builder != null;
    initialize(builder);
  }

  public static Builder builder() {  
    return new Builder();
  }

  public static TutorialComponent create() {  
    return builder().build();
  }

  private void initialize(final Builder builder) {  
    this.nonScopedServiceProvider = TutorialModule_NonScopedServiceFactory.create(builder.tutorialModule);
    this.mainActivityMembersInjector = MainActivity_MembersInjector.create((MembersInjector) MembersInjectors.noOp(), nonScopedServiceProvider);
  }

  @Override
  public void inject(MainActivity mainActivity) {  
    mainActivityMembersInjector.injectMembers(mainActivity);
  }

  public static final class Builder {
    private TutorialModule tutorialModule;
  
    private Builder() {  
    }
  
    public TutorialComponent build() {  
      if (tutorialModule == null) {
        this.tutorialModule = new TutorialModule();
      }
      return new DaggerTutorialComponent(this);
    }
  
    public Builder tutorialModule(TutorialModule tutorialModule) {  
      if (tutorialModule == null) {
        throw new NullPointerException("tutorialModule");
      }
      this.tutorialModule = tutorialModule;
      return this;
    }
  }
}

As you can see on lines 20-21, TutorialModule_NonScopedServiceFactory is being used as a Provider for instances of NonScopedService.

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

@Generated("dagger.internal.codegen.ComponentProcessor")
public final class TutorialModule_NonScopedServiceFactory implements Factory<NonScopedService> {
  private final TutorialModule module;

  public TutorialModule_NonScopedServiceFactory(TutorialModule module) {  
    assert module != null;
    this.module = module;
  }

  @Override
  public NonScopedService get() {  
    NonScopedService provided = module.nonScopedService();
    if (provided == null) {
      throw new NullPointerException("Cannot return null from a non-@Nullable @Provides method");
    }
    return provided;
  }

  public static Factory<NonScopedService> create(TutorialModule module) {  
    return new TutorialModule_NonScopedServiceFactory(module);
  }
}

The code on line 12 simply obtains an instance of NonScopedService from TutorialModule, 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 user-defined Module class.

Imagine that I would like to use the same instance of TutorialComponent to inject NonScopedService 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 Dagger 2 framework, and, therefore, is not aware of its scope. I include scope’s name in 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 TutorialModule class:

@Module
public class TutorialModule {

    @Provides
    public NonScopedService nonScopedService() {
        return new NonScopedService();
    }

    @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 = {TutorialModule.class})
public interface TutorialComponent {

    void inject(MainActivity mainActivity);

}

And the client that uses both services becomes:

public class MainActivity extends AppCompatActivity {

    @Inject NonScopedService mNonScopedService;
    @Inject SingletonScopedService mSingletonScopedService;

    private TutorialComponent mTutorialComponent;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        getTutorialComponent().inject(this);

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);
    }

    private TutorialComponent getTutorialComponent() {
        if (mTutorialComponent == null) {
            mTutorialComponent = DaggerTutorialComponent.builder()
                    .tutorialModule(new TutorialModule())
                    .build();
        }
        return mTutorialComponent;
    }
}

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

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

@Generated("dagger.internal.codegen.ComponentProcessor")
public final class DaggerTutorialComponent implements TutorialComponent {
  private Provider<NonScopedService> nonScopedServiceProvider;
  private Provider<SingletonScopedService> singletonScopedServiceProvider;
  private MembersInjector<MainActivity> mainActivityMembersInjector;

  private DaggerTutorialComponent(Builder builder) {  
    assert builder != null;
    initialize(builder);
  }

  public static Builder builder() {  
    return new Builder();
  }

  public static TutorialComponent create() {  
    return builder().build();
  }

  private void initialize(final Builder builder) {  
    this.nonScopedServiceProvider = TutorialModule_NonScopedServiceFactory.create(builder.tutorialModule);
    this.singletonScopedServiceProvider = ScopedProvider.create(TutorialModule_SingletonScopedServiceFactory.create(builder.tutorialModule));
    this.mainActivityMembersInjector = MainActivity_MembersInjector.create((MembersInjector) MembersInjectors.noOp(), nonScopedServiceProvider, singletonScopedServiceProvider);
  }

  @Override
  public void inject(MainActivity mainActivity) {  
    mainActivityMembersInjector.injectMembers(mainActivity);
  }

  public static final class Builder {
    private TutorialModule tutorialModule;
  
    private Builder() {  
    }
  
    public TutorialComponent build() {  
      if (tutorialModule == null) {
        this.tutorialModule = new TutorialModule();
      }
      return new DaggerTutorialComponent(this);
    }
  
    public Builder tutorialModule(TutorialModule tutorialModule) {  
      if (tutorialModule == null) {
        throw new NullPointerException("tutorialModule");
      }
      this.tutorialModule = tutorialModule;
      return this;
    }
  }
}

Take a close look at lines 21-23. While TutorialModule_NonScopedServiceFactory is directly used in MainActivity_MembersInjector, TutorialModule_SingletonScopedServiceFactory is wrapped by ScopedProvider (which is another implementation of Provider interface).

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

Let’s take a look at its implementation:

public final class ScopedProvider<T> implements Provider<T> {
  private static final Object UNINITIALIZED = new Object();

  private final Factory<T> factory;
  private volatile Object instance = UNINITIALIZED;

  private ScopedProvider(Factory<T> factory) {
    assert factory != null;
    this.factory = factory;
  }

  @SuppressWarnings("unchecked") // cast only happens when result comes from the factory
  @Override
  public T get() {
    // double-check idiom from EJ2: Item 71
    Object result = instance;
    if (result == UNINITIALIZED) {
      synchronized (this) {
        result = instance;
        if (result == UNINITIALIZED) {
          instance = result = factory.get();
        }
      }
    }
    return (T) result;
  }

  /** Returns a new scoped provider for the given factory. */
  public static <T> Provider<T> create(Factory<T> factory) {
    if (factory == null) {
      throw new NullPointerException();
    }
    return new ScopedProvider<T>(factory);
  }
}

Even though this code looks complicated, it is the standard approach to ensuring that only one instance of a 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 ScopedProvider 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 ScopedProvider makes use of a thread-safe “double check” idiom. This might be important if you intend to use TutorialComponent from multiple threads. However, in practice, I have never seen a reason to perform injection on non-UI thread in Android.

What would happen if I’d use the same instance of TutorialComponent 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 NonScopedService (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:

In this post we took an in-depth look at Dagger 2 scopes.

The summary of our observations:

  • Any time a non-scoped service is being injected by the same component, a new instance of a service is created.
  • 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 “is the injected service singleton” can be reduced to a question of “are instances of the service scoped and injected by the same component”.

That’s all I wanted to tell you about scopes. If you have comments or questions – please leave them below.

7 Comments

  1. Leonardo Miceli January 10, 2017 at 1:51 pm - Reply

    Thank you, finally a good explanation of how scopes work.
    Please keep up doing these deep analisys.

  2. Mohit April 3, 2017 at 4:47 am - Reply

    Nice explanation. Thanks.

  3. Ali April 23, 2017 at 12:23 pm - Reply

    Very good explanation. Thank you.

  4. Franklin Garcia August 18, 2017 at 11:22 pm - Reply

    The best explanation I’ve found so far. Thank you to walk us through the generated code for better understanding. Kudos!

  5. Hafiz Waleed Hussain November 17, 2017 at 1:37 am - Reply

    Good.

  6. Rakhi Dhavale February 28, 2018 at 11:39 am - Reply

    Very well explained ! Thank you !

  7. Maycon June 3, 2018 at 1:25 pm - Reply

    Awesome! Thank You!

Leave A Comment

Udemy course

 Dependency Injection
 with Dagger 2
 for Android Developers


 Highest Rated in Category
GET THE COURSE
close-link