Robopupu.Dependency API

Robopupu library has its own dependency injection (and pulling) framework. I have been using Dagger 2 quite a lot in my recent Android application projects. Dagger 2 is a really advanced and solid dependency injection framework, but based on my own experience it has some annoyances:

  1. Boiler plate code: One of the key objectives of Dagger 2 was to minimise the amount of required boiler plate code. Unfortunately that promise is not fully redeemed in practise. Developers need to write quite a lot of code in form of Dagger modules and components to implement dependency injection.
  2. Code generation: Dagger 2 generates quite many classes from the definitions given in Dagger modules and components. However, that is not a big issue, and the generated code is easy to understand and debug.
  3. Shortcoming with generics: Injecting classes with generics requires workarounds.
  4. Hard to use it correctly: Dagger 2 feels some times inflexible and has some shortcomings discussed for instance in here.

Because I have not been perfectly happy to develop with Dagger 2, I decided to implement my own DI framework that I could use (at least) in my own projects. I set the following key objectives for the development of Robopupu.Dependency API:

  1. The amount of boiler plate code that needs to be written should be as minimal as possible.
  2. The framework should feel more flexible to use than Dagger 2.
  3. The API should be easy to use and understand. Quite many developers have commented Dagger 2 to be a bit difficult to understand.
  4. Performance should be good. Dagger 2 has really good performance.
  5. Like Dagger 2, my solution should not use reflection, but should utilise annotation processor.

Key API Classes

The key classes of Robopupu.Dependency API are depicted in the following UML class diagram:

dependency_key_classes

  • DependencyScope: An object that is used to set up a scope for a group of dependencies that share the same lifecycle and have linked dependencies.
  • Dependency: This class provides an API consisting of static methods for requesting dependencies and for managing DependencyScopes.
  • D: A class extended from class Dependency for the convenience of lazy developers like me. You see, it takes a lot less typing to write D.get(Foo.class) instead of Dependency.get(Foo.class).
  • DependencyProvider: A base class for generated objects that provide dependencies via an implementation of DependencyScope. For instance, class  AboutFeatureScope uses a generated AboutFeatureScope_DependencyProvider to provide the scoped dependencies.
  • DependencyScopeOwner: An interface for an object that owns an implementation of DependencyScope and controls the lifecycle of it.
  • AppDependencyScope: A DependencyScope which has the same lifecycle as the application itself. An AppDependencyScope cannot be disposed like normal DependencyScopes.
  • @Scope: An annotation for declaring a DependencyScope class for triggering the  annotation processor implemented in Robopupu Compiler.
  • @Provides: An annotation for declaring a class, method, or constructor that provides a dependency of specified type. The annotation processor for Dependency API uses this annotation to code generate an implementation of DependencyProvider.

Dependency API annotations are not consistent with JSR-330 specification like the annotations of Dagger 2.

An Example

Let’s take a look how to use Robopupu.Dependency API for providing and obtaining dependencies. The discussed example is taken from the Robopupu sample application. The application has an About feature which is implemented using the classes depicted in the class diagram:

about_feature_dependencies

There are some dependencies between the classes shown in the diagram. For instance, class AboutFeatureImpl is dependent of PlatformManager, AboutFragment of AboutPresenter, and class AboutPresenterImpl has a dependency of type AboutView.

Class RobopupuAppScope is capable of providing an instance of AboutFeatureImpl as a dependency of type AboutFeatureRobopupuAppScope provides also an instance of PlatformManager as a dependency. RobopupuAppScope is a DependencyScope that has the same lifecycle as the application itself. Therefore, it is suitable for providing Feature  dependencies, such as an implementation for AboutFeature. To learn about Features (see Feature API).

All the View and Presenter classes implementing About Feature, are provided by the AboutFeatureScope. An instance of AboutFeatureScope is owned by an instance of AboutFeatureImpl. Class AboutFeatureImpl, like any other concrete Feature class, implements the DependencyScopeOwner interface. When a Feature is started, its DependencyScope is automatically activated by the FeatureManager (see Feature API).

Declaring a DependencyScope

Declaring an implementation of DependencyScope is pretty simple. Here is the class declaration of AboutFeatureScope which is extended from DependencyScope and declared for annotation processing using annotation @Scope:


package com.robopupu.feature.about;

import com.robopupu.api.dependency.DependencyScope;
import com.robopupu.api.dependency.Scope;

@Scope
public class AboutFeatureScope extends DependencyScope {
}

Annotation @Scope is needed to trigger the annotation processor to generate class AboutFeatureScope_DependencyProvider that will contain the actual code for created and providing dependency instances. The code generation is provided once again to minimise the amount of boiler plate code to be written when using Dependency API.

Declaring the Provided Dependencies

Let’s take a look how the dependency of type AboutView is declared to be provided by class AboutFragment:


package com.robopupu.feature.about.view;
...
@Plugin
public class AboutFragment extends CoordinatorLayoutFragment<AboutPresenter> 
    implements AboutView {

    @Plug AboutPresenter mPresenter;

    private Binding mVersionTextBinding;

    @Provides(AboutView.class)
    public AboutFragment() {
        super(R.string.ft_about_title);
    }
    ...
}

We simply annotate the public constructor of AboutFragment with @Provides annotation and give the  type of the provided dependency (i.e. AboutView.class) as an annotation parameter. However, in case the type of provided dependency is the same as the annotated constructor or class, we can leave out the annotation parameter.

An alternative for annotating a public constructor of a class with @Provides is to annotate the class itself:


package com.robopupu.feature.about.view;
...

@Plugin
@Provides(AboutView.class)
public class AboutFragment extends CoordinatorLayoutFragment<AboutPresenter> 
    implements AboutView {

    @Plug AboutPresenter mPresenter;

    private Binding mVersionTextBinding;

    public AboutFragment() {
        super(R.string.ft_about_title);
    }
    ...
}

It is also possible to annotate a method to provide a dependency, but in that case the annotated method has to be placed into an implementation of DependencyScope. Let’s assume that we have a dependency of type Foo to be provided in the scope of About Feature. Foo is an interface whose concrete implementing class is FooImpl. To provide dependency Foo we could add method  provideFoo() to class AboutFeatureScope:


package com.robopupu.feature.about;
import com.robopupu.api.dependency.DependencyScope;
import com.robopupu.api.dependency.Scope;

@Scope
public class AboutFeatureScope extends DependencyScope {
    @Provides(Foo.class)
    public FooImpl provideFoo() {
        return new FooImpl();
    }
}

It is also possible to have linked dependencies. Let’s say that we need to provide a dependency of type class Bar. To create an instance of Bar we need to pass an instance  of Foo as a parameter. Dependency API is capable of dealing with linked dependencies, assuming that all the linked dependencies are provided.


package com.robopupu.feature.about;
...
public class Bar {
    ...
    @Provides
    public Bar(final Foo foo) {
        ...
    }
    ...
}

Note that we did not specify explicitly what would be the dependency scope for the provided dependencies: AboutView, Foo, and Bar. The reason is that the annotation processor for the Dependency API knows the class packages of the elements annotated with @Provides. And if those annotations do not specify the dependency scope explicitly, the annotation processor scans the parent packages for a DependencyScope class annotated with @Scope. The first annotated DependencyScope class that is found when traversed the package hierarchy is upwards, is assumed to be the dependency scope that is to provide the dependency. The motivation for this implicit scoping is once again the objective to minimise the amount of boiler plate code to be written.

It is also possible and sometimes necessary to define the target dependency scope explicitly. This is the case when we want to provide dependency AboutFeature via the application scope RobopupuAppScope. To specify the target DependencyScope, we add the following @Scope annotation in addition to @Provides annotation:


package com.robopupu.feature.about;
...
@Plugin
public class AboutFeatureImpl extends AbstractFeature 
    implements AboutFeature, AboutPresenterListener {

    @Plug PlatformManager mPlatformManager;

    @Scope(RobopupuAppScope.class)
    @Provides(AboutFeature.class)
    public AboutFeatureImpl() {
        super(AboutFeatureScope.class);
    }
    ...
}

 

Obtaining a Dependency

Now we have presented the basics for declaring and defining DependencyScopes, and for providing dependencies for them. Next, let’s take a look how to obtain a dependency of a given type. Obtaining a dependency is pretty simple thing to do. As an example, see the following code that obtains a dependency of type LicenseInfoPresenter in method onShowLicenseInfo() of AboutFeatureImpl. We simply use the static method get(Class) of class D (which is a convenience class extended from class Dependency).


...
@Override
public void onShowLicenseInfo() {
    final Params params = new Params(LicensesInfoPresenter.KEY_PARAM_LICENSE_URL, 
        mPlatformManager.getString(R.string.robopupu_license_file));
    final LicensesInfoPresenter presenter = D.get(LicensesInfoPresenter.class);
    ...
}
...

Note that we can obtain dependencies using the static get(…) methods in any object in our application without injecting first the requesting object itself to an object graph. In Dagger 2, field injection works only for the objects that are created by the Dagger or for the object that are explicitly injected using a Dagger component.

There are quite many variants of the Dependency.get(…) methods:


/**
 * Gets a requested dependency of the specified type. The dependency is requested from
 * the currently active {@link DependencyScope}.
 *
 * @param dependencyType A {@link Class} specifying the type of the requested dependency.
 * @param <T>            A type parameter for casting the requested dependency to expected type.
 * @return The requested dependency. If {@code null} is returned, it indicates an error in
 * an {@link DependencyScope} implementation.
 */
public static <T> T get(Class<T> dependencyType) ...

/**
 * Gets a requested dependency of the specified type. The dependency is requested from
 * the given {@link DependencyScope}.
 *
 * @param scopeType A {@link Class} specifying {@link DependencyScope}.
 * @param dependencyType A {@link Class} specifying the type of the requested dependency.
 * @param <T>            A type parameter for casting the requested dependency to expected type.
 * @return The requested dependency. If {@code null} is returned, it indicates an error in
 * an {@link DependencyScope} implementation.
 */
public static <T> T get(Class<? extends DependencyScope> scopeType, 
    Class<T> dependencyType) ...

/**
 * Gets a requested dependency of the specified type. The dependency is requested from
 * the specified {@link DependencyScope}.
 *
 * @param scopeClass A {@link Class} specifying {@link DependencyScope}.
 * @param dependencyType A {@link Class} specifying the type of the requested dependency.
 * @param dependant      The object requesting the requested. This parameter is required when the requesting object
 *                       is also a requested within the object graph represented by the active {@link Dependency}.
 * @param <T>            A type parameter for casting the requested dependency to expected type.
 * @return The requested dependency. If {@code null} is returned, it indicates an error in
 * an {@link DependencyScope} implementation.
 */
public static <T> T get(Class<? extends DependencyScope> scopeClass, 
    Class<T> dependencyType, Object dependant) {

/**
 * Gets a requested dependency of the specified type. The dependency is requested from
 * the given {@link DependencyScope}.
 *
 * @param scope A {@link DependencyScope}.
 * @param dependencyType A {@link Class} specifying the type of the requested dependency.
 * @param <T>            A type parameter for casting the requested dependency to expected type.
 * @return The requested dependency. If {@code null} is returned, it indicates an error in
 * an {@link DependencyScope} implementation.
 */
public static <T> T get(DependencyScope scope, Class<T> dependencyType) ...

/**
 * Gets a requested dependency of the specified type. The dependency is requested from
 * the currently active {@link DependencyScope}.
 *
 * @param dependencyType A {@link Class} specifying the type of the requested dependency.
 * @param dependant      The object requesting the requested. This parameter is required when the requesting object
 *                       is also a requested within the object graph represented by the active {@link Dependency}.
 * @param <T>            A type parameter for casting the requested dependency to expected type.
 * @return The requested dependency. If {@code null} is returned, it indicates an error in
 * an {@link DependencyScope} implementation.
 */
public static <T> T get(Class<T> dependencyType, Object dependant) ...

/**
 * Gets a requested dependency of the specified type. The dependency is requested from
 * the currently active {@link DependencyScope}.
 *
 * @param scope A {@link DependencyScope}.
 * @param dependencyType A {@link Class} specifying the type of the requested dependency.
 * @param dependant      The object requesting the requested. This parameter is required when the requesting object
 *                       is also a requested within the object graph represented by the active {@link Dependency}.
 * @param <T>            A type parameter for casting the requested dependency to expected type.
 * @return The requested dependency. If {@code null} is returned, it indicates an error in
 * an {@link DependencyScope} implementation.
 */
public static <T> T get(DependencyScope scope, Class<T> dependencyType, 
    Object dependant) ...

Activating and Deactivating DependencyScopes

As shown above, there are several get(…) methods for which an instance of DependencyScope can be given as a parameter. Generally, a developer should not give a DependencyScope as a parameter to get(…) method invocations when requesting dependencies, because this will make the requested dependencies dependant of the given DependencyScope.

If a DependencyScope is not provided explicitly, the DependencyScope that is capable of providing the requested dependencies needs to be the active one. Activation is done using the static method Dependency#activateScope(DependencyScopeOwner) .

When a DependencyScope is not needed anymore, it should be deactivated using method Dependency#deactivateScope(DependencyScopeOwner). It is crucial to deactivate  a DependencyScope in order to avoid memory leaks and to minimise usage of memory. A deactivated DependencyScope is disposed so that the dependencies will be garbage collected. This is why the methods for activating and deactivating DependencyScopes take an instance of DependencyScopeOwner as a parameter instead of the DependencyScope. A DependencyScopeOwner instance is responsible for taking care of the lifecycle of the DependencyScope it owns.