@ActivityScope with Dagger 2

  • Tutorial
Hello, Habr! I want to share the experience of creating ActivityScope. Those examples that I saw on the Internet, in my opinion, are not complete enough, irrelevant, artificial and do not take into account some of the nuances of practical development.

The article assumes that the reader is already familiar with Dagger 2 and understands what a component, module, injection, and object graph are and how it all works together. Here, first of all, we will concentrate on creating an ActivityScope and how to link it to fragments.

So let's go ... What is scope?



Scope is a Dagger 2 mechanism that allows you to save a certain set of objects, which has its own life cycle. In other words, scope is a graph of objects that has its own lifetime, which depends on the developer.

By default, Dagger 2 out of the box provides us with javax.inject.Singleton scope support . As a rule, objects in this scope exist exactly as long as the instance of our application exists.

In addition, we are not limited in the possibility of creating our own additional scopes. A good example of custom scopes is UserScope.whose objects exist as long as the user is authorized in the application. As soon as the user session ends, or the user explicitly exits the application, the object graph is destroyed and recreated at the next authorization. In such a scope, it is convenient to store objects associated with a specific user and not meaningful to other users. For example, some AccountManager that allows you to view lists of accounts of a specific user.



The figure shows an example of the Singleton and UserScope life cycles in an application.

  • When launched, a Singleton scope is created , whose lifetime is equal to the lifetime of the application. In other words, objects belonging to the Singleton Scope will exist until the system destroys and unloads our application from memory.
  • After starting the application, User1 is authorized in the application. At this point, a UserScope is created containing objects that make sense for the user.
  • After a while, the user decides to "log out" and logs out of the application.
  • Now User2 is authorized and this initiates the creation of UserScope objects for the second user.
  • When a user session expires, it destroys the graph of objects.
  • User User1 returned to the application log, thereby creating objects Count UserScope and sends the application to the background is.
  • After some time, the system in a situation of lack of resources makes a decision to stop and unload from the memory of our application. This results in the destruction of both UserScope and SingletonScope .

Hopefully with the scopes a bit sorted out.

We now turn to our example - ActivityScope . In real Android applications, ActivityScope can be extremely useful. Still would! It is enough to imagine some kind of complex screen consisting of a bunch of classes: heels of various fragments, a bunch of adapters, helpers and presenters. In this case, it would be ideal to “fumble” between them a model and / or classes of business logic, which should be common.

There are 3 options for solving this problem:

  1. Use self-made singletons, Application class or static variables to transfer references to common objects. I definitely don’t like this approach, because it violates the principles of OOP and SOLID, makes the code confusing, difficult to read and unsupported.

  2. Independently transfer objects from Activity to the necessary classes through setters or constructors. The disadvantage of this approach is the cost of writing routine code, when instead it would be possible to focus on writing new features.

  3. Use Dagger 2 to inject shared objects into the necessary places in our application. In this case, we get all the advantages of the second approach, while not wasting time writing template code. In fact, we are shifting the writing of middleware to the library.

Let's take a look at the steps to create and use ActivityScope using Dagger 2.

So, to create a custom scope you need:

  • Declare scope (create annotation)
  • Declare at least one component and the corresponding module for the scope
  • At the right moment, instantiate the object graph and delete it after use

The interface of our demo application will consist of two screens ActivityА and ActivityB and a common fragment used by both SharedFragment activities .



The application will have 2 scopes: Singleton and ActivityScope .

Conventionally, all of our bins can be divided into 3 groups:

  • Singleton - SingletonBean
  • Activity bean scopes that are needed only inside the activity - BeanA and BeanB
  • Activation bean scopes, access to which is needed both from the activity itself and from other places of the activity activopoope, for example, a fragment - SharedBean

Each bean gets a unique id upon creation. This allows you to clearly understand whether the scope works as intended, because each new bean instance will have an id different from the previous one.



Thus, in the application there will be 3 graphs of objects (3 components)

  • SingletonComponent - a graph of objects that exist while the application is running and not killed by the system
  • ComponentActivityA - a graph of objects necessary for the operation of ActivityA (including its fragments, adapters, presenters, etc.) and existing as long as there is an instance of ActivityA. If you destroy and recreate the activity, the graph will also be destroyed and recreated along with a new instance of the activity. This graph is a superset that includes all objects from the Singleton scopes.
  • ComponentActivityB is a similar graph, but for ActivityB



Let's move on to implementation. To get started, connect Dagger 2 to our project. To do this, connect the android-apt plugin in the root build.gradle ...

buildscript {
   //...
   dependencies {
      //...
       classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
   }
}

and Dagger 2 itself in app / build.gradle

dependencies {
   compile 'com.google.dagger:dagger:2.7'
   apt 'com.google.dagger:dagger-compiler:2.7'
}

Next, we declare a module that will provide singleton

@Module
public class SingletonModule {
   @Singleton
   @Provides
   SingletonBean provideSingletonBean() {
       return new SingletonBean();
   }
}

and singleton component:

@Singleton
@Component(modules = SingletonModule.class)
public interface SingletonComponent {
}

We create an injector - the only singleton in our application that we will control, not Dagger 2, and which will hold the Singleton scopes of the dagger and be responsible for the injection.

public final class Injector {
   private static final Injector INSTANCE = new Injector();
   private SingletonComponent singletonComponent;
   private Injector() {
       singletonComponent = DaggerSingletonComponent.builder()
               .singletonModule(new SingletonModule())
               .build();
   }
   public static SingletonComponent getSingletonComponent() {
       return INSTANCE.singletonComponent;
   }
}

Declare ActivityScope . In order to declare your scope, you need to create an annotation with the name of the scope and mark it with the javax.inject.Scope annotation .

@Scope
public @interface ActivityScope {
}

Group beans into modules: shared and for activities

@Module
public class ModuleA {
   @ActivityScope
   @Provides
   BeanA provideBeanA() {
       return new BeanA();
   }
}
@Module
public class ModuleB {
   @ActivityScope
   @Provides
   BeanB provideBeanB() {
       return new BeanB();
   }
}
@Module
public class SharedModule {
   @ActivityScope
   @Provides
   SharedBean provideSharedBean() {
       return new SharedBean();
   }
}

We declare the corresponding components of the activities. In order to implement a component that will include objects of another component, there are 2 ways: subcomponents and component dependencies . In the first case, child components have access to all objects of the parent component automatically. In the second - in the parent component, you must explicitly specify the list of objects that we want to export to children. In the framework of one application, in my opinion, it is more convenient to use the first option.

@ActivityScope
@Subcomponent(modules = {ModuleA.class, SharedModule.class})
public interface ComponentActivityA {
   void inject(ActivityA activity);
   void inject(SharedFragment fragment);
}
@ActivityScope
@Subcomponent(modules = {ModuleB.class, SharedModule.class})
public interface ComponentActivityB {
   void inject(ActivityB activity);
   void inject(SharedFragment fragment);
}

In the created subcomponents, we declare the injection points. In our example, there are two such points: Activity and SharedFragment . They will share SharedBean shared beans .

Subcomponent instances are obtained from the parent component by adding objects from the subcomponent module to an existing graph. In our example, the parent component is SingletonComponent , add methods for creating subcomponents to it.

@Singleton
@Component(modules = SingletonModule.class)
public interface SingletonComponent {
   ComponentActivityA newComponent(ModuleA a, SharedModule shared);
   ComponentActivityB newComponent(ModuleB b, SharedModule shared);
}

That's all. The entire infrastructure is ready, it remains to instantiate the declared components and inject dependencies. Let's start with the fragment.

A fragment is used immediately inside two different activities, so it does not need to know specific details about the activity within which it is located. However, we need access to the activity component in order to get access to the graph of objects of our scopes through it. To solve this “problem”, we use the Inversion of Control pattern , creating an intermediate InjectorProvider interface through which the interaction with activities will be built.

public class SharedFragment extends Fragment {
   @Inject
   SharedBean shared;
   @Inject
   SingletonBean singleton;
   //…
   @Override
   public void onAttach(Context context) {
       super.onAttach(context);
       if (context instanceof InjectorProvider) {
           ((InjectorProvider) context).inject(this);
       } else {
           throw new IllegalStateException("You should provide InjectorProvider");
       }
   }
   public interface InjectorProvider {
       void inject(SharedFragment fragment);
   }
}

It remains to instantiate the components of the ActivityScope level inside each of the activities and inject the activity and the fragment contained inside it

public class ActivityA extends AppCompatActivity implements SharedFragment.InjectorProvider {
   @Inject
   SharedBean shared;
   @Inject
   BeanA a;
   @Inject
   SingletonBean singleton;
   ComponentActivityA component =
           Injector.getSingletonComponent()
                   .newComponent(new ModuleA(), new SharedModule());
  //...
   @Override
   public void inject(SharedFragment fragment) {
       component.inject(this);
       component.inject(fragment);
   }
}

I will reiterate the main points:

  • We created 2 different scopes: Singleton and ActivityScope
  • ActivityScope is implemented through Subcomponent , not component dependencies, so that you don’t have to explicitly expose all beans from Singleton scopes
  • Activity stores a link to a graph of objects of the corresponding ActivityScop and injects itself and all classes that want to inject beans from ActivityScope, for example, SharedFragment
  • With the destruction of activity, the graph of objects for a given activity is also destroyed.
  • The Singleton graph of objects exists as long as the application instance exists.

At first glance, it might seem that to implement such a simple task, you need to write quite a lot of connecting code. In the demo application, the number of classes that perform the “work” (bins, fragments, and activities) is approximately comparable to the number of “connecting” classes of the dagger. But:

  • In a real project, the number of “working” classes will be much larger.
  • It is enough to write the connecting code once, and then simply add the necessary components and modules.
  • Using DI greatly facilitates testing. You have additional opportunities for injecting mok and stubs instead of real bins during testing
  • The business logic code becomes more isolated and concise due to the transfer of middleware and instance code into dagger classes. At the same time, only business logic and nothing more remains in the business logic classes themselves. Such classes are again easier to write, maintain, and cover with unit tests.

»Demo project is available on github.

Everyone Dagger and happy coding! :)

Also popular now: