Implementing ‘Role-Specific’ Scopes of Dagger2

rubicon_dev

RUBICON

Posted on April 29, 2021

Implementing ‘Role-Specific’ Scopes of Dagger2

Research, Research & more Research

A common question I get asked is:

“How did you start implementing this process into your own work?”

My answer is purely based on researching and using trial and error. Now I’m here to bring this information to your fingertips. After reading about Dagger2 and the implementation process, I began experimenting with sample applications.

Where can this method be found?

One example of where this method can be found is on online discussion boards.

For example, if you are a simple user then you will see the topics as you normally would. However, if you are a user with higher privileges, you will see all of the topics even including the hidden ones. Basically, the idea behind this is to provide different dependencies for different user roles without changing the UI or your Domain Logic.

Pretty cool, right?

Before we begin with the most exciting part of the blog, it is important to explain some core concepts for a better understanding.

Firstly, in order to understand the framework of Dagger2, you should be familiar with the technique of Dependency Injection.

I will give you a walk through before we get into the fun stuff.

Dependency Injection

We all love wikipedia, so here is a definition straight from the source:

“In the world of software engineering, Dependency Injection is a technique where one object (or static method) supports the dependencies of another object. What is a dependency? A dependency is an object that can be used as a service. The injection is the passing of a dependency to a dependent object (a client that would use it).”

Confusing? A little bit. If I were to come up with my own definition of Dependency Injection, it would be:

“A Dependency Injection is a technique where one object (dependent) gets an instance (inject) of another object (dependency) from somewhere else prior to creating it itself.”

Here is a visual representation of Dependency Injection for all of you visual learners out there:

Alt Text

Now that we understand the technique of Dependency Injection, we can move on to the most important part of the blog-the framework of Dagger 2.

Dagger2

What is Dagger2? Google’s Github explains Dagger2 as a fully static, compile-time evolution approach Dependency Injection that is used for both Java and Android. It is a readjustment of a version created by Square and is now supported by Google.

Dagger2 focuses on addressing development and performance issues that have plagued reflection-based solutions.

The Standard Approach

Before explaining the implementation of ‘Role-Specific Scopes’ of Dagger2, I would like to share the standard approach and compare.

To this day, this approach is widely used.

In order to achieve Dependency Injection through this approach, the process is usually separated into two steps:

Declare the @scope which indicates a level of nesting. A @scope is an annotation provided by the Dagger2 Framework. Although we’d have @ActivityScope for Activities and @FragmentScope for Fragments, there is no need to declare the Scope for Application. This is because it already exists under the name @singleton which is also provided by Dagger2.

Note:The Scopes can be called however you want as long as the names make sense. In my own code, I usually use the name @PerActivity in favour of @ActivityScope and @PerFragment in favour of @FragmentScope.
Properly apply our custom scopes through @ContributesAndroidInjection or via @SubComponent.
You can read more about it here under Injection Activity Objects.

If we were to draw a simple graphic representation of dependencies, then it would look like this:

Alt Text

This diagram shows AppComponent which consists of the three following different modules:

  • AppModule - We are able to define app singletons within it, such as ApplicationContext.
  • DataModule - It provides us with Repository implementations, UseCases/Interactors and services like Retrofit. Ideally, this would be placed in a separate module if we were following CLEAN Architecture.
  • ActivityBindingModule - Google has introduced the @ContributesAndroidInjector annotation in the 2.12 version of Dagger2. The purpose of it is to simplify the creation process of SubComponents. Inside it we have multiple @ContributedAndroidInjector annotations which create multiple activity-level SubComponents (hence annotation @ActivityScope).

Note: There is also a AndroidSupportInjectionModule/AndroidInjectionModule. It helps us connect Dagger2 dependencies to the Android Front-end (Activity, Fragment, Service, etc.). You can use DaggerActivity, DaggerFragment, DaggerApplication to reduce the boilerplate in your Activity/Fragment/Application. You can also use AndroidInjector in your dagger components to reduce the boilerplate as well. There is no need to name this module as it is already provided by Dagger2, therefore it isn’t displayed on the diagram.

Alternative Approach: ‘Role-Specific Scopes’ Implementation

Now for the real fun part, I’ll finally be explaining the approach that I truly recommend - the implementation of 'Role-Specific' Scopes.

Let’s start with answering the most important question: WHY? Why is it useful for anyone to use? Here are a few key points:

  1. It is efficient for long-term maintenance in complex systems.
  2. Promotes Separation of Concerns.
  3. Better readability - Imagining a dependency graph can be tough. Therefore, by specifying different implementations the package structure along with the code becomes highly readable.
  4. Promotes SRP, DIP and DRY principles.
  5. Goes well with CLEAN Architecture.

After listing the reasons why this approach is useful, it is important to note that this level of abstraction can cause bottleneck for apps that don’t rely on User Scopes as often and prolong development time. As well, the learning curve is a bit heavy for Dagger2 itself and this approach can make the learning curve even heavier.

The whole idea behind Role-Scope Implementation is to provide different Repository/Facade implementations for different users. That being said, we should declare @scope for each user role (eg. @UserScope, @AdminScope).

The Dependency Inversion Principle states that high-level modules should not depend on low-level modules. Instead, they should depend on abstractions, not on concrete implementations.

If we were to apply DIP to our Repository, we could use Dagger2 to provide different implementations while still depending on abstraction. The Repository is the single source of truth for the consumer (presenter, controller, etc). The consumer doesn't care about how the repository gets the data. The repository implementation handles logic. Here's a visual representation:

Alt Text

Now I’ll show you how our Repository looks through code:

interface ITopicRepository {
    fun loadTopics(): List<Topic>
}
Enter fullscreen mode Exit fullscreen mode

If we were to implement 'Role-Specific' Scopes, we would need to have the exact same number of implementations of given Repository. Let’s say we’ve implemented @AdminScope and @UserScope on Topic Repository, then we’d need to have AdminTopicRepository and UserTopicRepository.

The code would look something like this:

/**
 * Admin instance of ITopicRepository
 */
class AdminTopicRepository : ITopicRepository {
    override fun loadTopics(): List<Topic> {
        return loadAdminTopics()
    }
}

/**
 * User instance of ITopicRepository
 */
class UserTopicRepository : ITopicRepository {
    override fun loadTopics(): List<Topic> {
        return loadUserTopics()
    }
}
Enter fullscreen mode Exit fullscreen mode

But how do you achieve this dependency injection through Dagger2? The following graph will explain it for us:

Alt Text

You can see that for this specific example we’ve ditched the DataModule completely. Remember that this will not always be the case if we were to have dependencies that wouldn't rely on different scopes, but for purpose of this blog there aren’t such dependencies.

Our ActivityBindingModule now produces SubComponents annotated with different levels of User Scopes in favour of the nesting-level scope. However, our SubComponents now have dependency on {Scope}Module and Activity/FragmentModule. {Scope}Module is the one who holds different implementations of Repository.

Alt Text

Alright, here we go.

Our AppComponent code looks like this:

@Singleton
@Component(
    modules = [
        AndroidSupportInjectionModule::class,
        AppModule::class,
        ActivityBindingModule::class]
)
interface AppComponent : AndroidInjector<DiscussionBoardApp> {

    @Component.Builder
    abstract class Builder : AndroidInjector.Builder<DiscussionBoardApp>()
}
Enter fullscreen mode Exit fullscreen mode
@Module
interface ActivityBindingModule {

    /**
     * Binds dependencies consumed by UserBoardActivity
     * @return UserBoardActivity
     */
    @UserScope
    @ContributesAndroidInjector(modules = [UserBoardModule::class, UserModule::class])
    fun bindUserBoard(): UserBoardActivity

    /**
     * Binds dependencies consumed by AdminBoardActivity
     * @return AdminBoardActivity
     */
    @AdminScope
    @ContributesAndroidInjector(modules = [AdminBoardModule::class, AdminModule::class])
    fun bindAdminBoard(): AdminBoardActivity
}
Enter fullscreen mode Exit fullscreen mode

And our Role-Scope module looks like this:

@Module
class AdminModule {

    /**
     * Provides Admin instance of ITopicRepository.
     * @return ITopicRepository
     */
    @AdminScope
    @Provides
    fun provideTopicRepository(): ITopicRepository {
        return AdminTopicRepository()
    }

    /**
     * Provides Admin instance of LoadTopics UseCase.
     * @param topicRepository ITopicRepository
     * @return LoadTopics
     */
    @AdminScope
    @Provides
    fun provideLoadTopicsUseCase(topicRepository: ITopicRepository): LoadTopics {
        return LoadTopics(topicRepository)
    }
}
Enter fullscreen mode Exit fullscreen mode
@Module
class UserModule {

    /**
     * Provides User instance of ITopicRepository.
     * @return ITopicRepository
     */
    @UserScope
    @Provides
    fun provideTopicRepository(): ITopicRepository {
        return UserTopicRepository()
    }

    /**
     * Provides User instance of LoadTopics UseCase.
     * @param topicRepository ITopicRepository
     * @return LoadTopics
     */
    @UserScope
    @Provides
    fun provideLoadTopicsUseCase(topicRepository: ITopicRepository): LoadTopics {
        return LoadTopics(topicRepository)
    }
}
Enter fullscreen mode Exit fullscreen mode

Let's Conclude

I hope this code was helpful and that you’ve learned something new after reading this blog. If you have any questions or would like more guidance or support throughout the process, I’d be happy to help. I’d also love to discuss your thoughts and learn about what approaches you find valuable.

I encourage you to check out the code itself in action.

Dagger2 is a powerful Dependency Injection framework and if used properly it can provide efficiency for development and code maintenance. Happy coding!


Original blog post: Implementing ‘Role-Specific’ Scopes of Dagger2

💖 💪 🙅 🚩
rubicon_dev
RUBICON

Posted on April 29, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related