ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Follow publication

Finding unnecessary Component Dependencies with Dagger SPI

Many Android developers use Dagger or its “wrapper” Hilt for Dependency Injection. But not many of them use Dagger SPI. This mechanism gives us access to the dependency graph, allowing us to add our own graph checks and much, much more. In this article, I want to discuss working with Dagger SPI using the example of finding unused Component Dependencies. After reading this article, you will be able to find them. Or, if you prefer, you can write your own dependency graph checks. Or whatever floats your boat.

Let me introduce myself properly — my name is Danil Perevalov, and now let’s move on to the topic.

Dependencies

To begin with, let’s look at what Component Dependencies are and how they are used. Suppose we have some Component called FeatureComponent. It is responsible for storing and sending data about the current device to the server. Consequently, we will need dependencies for:

  • storing data, for example, in a database;
  • obtaining information about the current device;
  • sending data to the server, essentially, we need a network client.

We can provide these dependencies to our Component either by using the SubComponent hierarchy or by using Component Dependencies. The second option is what we are interested in today.

From the perspective of Dagger, this can be implemented in two methods: through a single common interface or through a set of interfaces. I won’t speculate on which method is better in which situation. I will just present both methods for a better understanding of the situation.

Method 1. Common interface

We create a common interface in which we describe all the dependencies that may be needed in the current component.

interface FeatureComponentDependencies {

fun provideDeviceInfoManager(): DeviceInfoManager
fun provideDatabaseManager(): DatabaseManager
fun provideNetworkManager(): NetworkManager
}

The next thing we do is simply specify this interface in the dependencies field of the Component annotation.

@Component(
dependencies = [
FeatureComponentDependencies::class
]
)

internal interface FeatureComponent

Then, someone somewhere creates an implementation of this interface specifically for our Component, and injects this implementation into the Component.Builder or Component.Factory during its creation.

Now, let’s move on to the second method.

Method 2. Set of interfaces

For each type of dependency, we create a separate interface: one for device information — CoreDeviceDependencies, one for databases — CoreDatabaseDependencies, and one for network operations — CoreNetworkDependencies.

interface CoreDeviceDependencies {

fun provideDeviceInfoManager(): DeviceInfoManager
}

interface CoreDatabaseDependencies {

fun provideDatabaseManager(): DatabaseManager
}

interface CoreNetworkDependencies {

fun provideNetworkManager(): NetworkManager
}

Next, we specify our interfaces in the dependencies field of the Component annotation.

@Component(
dependencies = [
CoreDeviceDependencies::class,
CoreDatabaseDependencies::class,
CoreNetworkDependencies::class,
]
)

internal interface SomeComponent

The implementations of these interfaces are created not specifically for our Component, but for the Components of all potential features. Then, when we create our Component, we simply inject those implementations into it.

And everything seems fine, but things change over time…

Unused dependencies

And so does our Component. Suppose, for FeatureComponent, we decided not to send device information to the server. Now we just collect and store information. We removed the code responsible for sending, but forgot to remove the declaration of CoreNetworkDependencies from our FeatureComponent. Obviously, this is strange for a feature that literally did three actions, but in a real application the features are usually much larger, so it’s very easy to forget to remove some Component Dependency.

Besides the fact that our FeatureComponent becomes a bit cluttered, in a multi-module application this can lead to unnecessary dependencies remaining between modules. This affects:

  • the cold build time. May form bottlenecks between modules due to unnecessary dependencies.
  • the hot build time. In our case, we no longer need CoreNetworkDependencies, but because of them, our feature module will be forced to connect to the network module. And if there are changes in the network module, our module will also be forced to rebuild, even though it doesn’t need to.

And, generally, leaving code that does nothing isn’t very programmer-like. It is obvious that they must be found for later removal. Unfortunately, Dagger cannot do anything like this. But Dagger has Dagger SPI, which provides us with a build graph that makes it easy to implement the search for unnecessary Component Dependencies.

Search

I want to clarify that we will perform the search using kapt, as the version of Dagger with KSP is still in alpha and has bugs. And honestly, not much will change in our Dagger SPI plugin when using KSP.

Btw, we will perform the search for the second method, which is a set of interfaces. The whole thing is that the code for searching for unused dependencies in the second method includes the code from the first method. And I don’t really want to go through that again.

Dagger SPI

So how does this Dagger SPI work?

It’s pretty simple. First, we create a new library module and add kotlin(“jvm”) instead of plugin id(“com.android.library”).

We add Dagger, Dagger SPI and AutoService to the dependencies. It goes like this:

plugins {
kotlin("jvm")
kotlin("kapt")
}

dependencies {
implementation("com.google.dagger:dagger:2.44")
implementation("com.google.dagger:dagger-spi:2.44")
compileOnly("com.google.auto.service:auto-service:1.1.1")
kapt("com.google.auto.service:auto-service:1.1.1")
}

BindingGraphPlugin

In the new module we create a new class, in our case it will be DaggerUnusedValidator. We inherit it from the BindingGraphPlugin and add the @AutoService(BindingGraphPlugin::class) annotation. This annotation will allow Dagger SPI to find all of its plugins.

@AutoService(BindingGraphPlugin::class)
class DaggerUnusedValidator : BindingGraphPlugin {

override fun visitGraph(
bindingGraph: BindingGraph,
diagnosticReporter: DiagnosticReporter
)
{
}
}

There are two parameters in the visitGraph method: bindingGraph and diagnosticReporter. The first parameter contains the dependency graph, while the second one is responsible for conveniently logging issues with graph elements.

It is important to note that the visitGraph method will be called for each individual Component and Module, and possibly even multiple times, as there may be multiple rounds in code generation. Separate visitGraph calls make sense because, from Dagger’s perspective, each individual Component and Module represents separate dependency graphs that we later connect to each other.

All that is left is to connect our module to each module that uses Dagger.

"kapt"(project(":kapt_validate_dagger_deps"))

And now, when building the project, after generating the Dagger’s code, the visitGraph method will get the finished dependency graph.

But so far, our method does nothing. It’s time to fix that!

Finding Components

The first thing we need to do, of course, is get the Component.

It’s very easy to do. The BindingGraph already has a special rootComponentNode method. But, as I mentioned earlier, the visitGraph method can receive the Module in addition to the “real” Component. To distinguish between them, let’s use the isRealComponent flag.

val currentComponent = bindingGraph.rootComponentNode()
if (!currentComponent.isRealComponent) {
return
}

The name isRealComponent makes it clear that this flag is only true for “real” Components. That’s exactly what we need.

We have the required Component, and now we need to somehow extract Component Dependencies from it.

Attempting to extract Dependencies

Unfortunately, Dagger SPI returns us an already assembled graph. This means that there cannot be any unused Component Dependencies in it. So we will have to put in some effort to extract them.

But! We can get TypeElement of our Component through componentPath().currentComponent() methods.

In terms of kapt code generation, TypeElement is something like Class. Finally, classes don’t exist as such at the code generation stage, they appear at Runtime. (This, btw, allows us to access the Runtime classes of the code generator itself.)

So the plan is straightforward: we get the TypeElement of our Component, from it we get the Component annotation, and then we access the dependencies field of that annotation. Sounds simple. And this is how it works:

val currentComponent = componentNode.componentPath().currentComponent()
val dependencies = currentComponent.getAnnotation(Component::class.java).dependencies

But no such luck! This code won’t work. When attempting to access the dependencies field, we will get a MirroredTypeException.

Problem with MirroredTypeException

This error occurs because the Class[] type is specified in the dependencies field of the Component annotation. As I mentioned earlier, classes don’t exist during code generation. If the Component annotation contained an array of strings with class names instead of the Class array, this problem wouldn’t occur. You can read more about this phenomenon in this article, if you’re interested in it.

And you may say: “But Dagger somehow manages it!”. That’s true. The fact that Dagger works is an indication that there is a method to circumvent this problem. However, this method is a bit… freaky.

Now the plan isn’t that simple, so let’s break it down into two stages:

Stage 1. Finding the Dependencies method

  1. Get all annotations via annotationMirrors.
  2. Find the Component annotation among them.
  3. Find the dependencies method in it.

Stage 2. Getting the dependencies types

  1. Get the list of Component Dependencies as AnnotationValue from the method.
  2. Get the value from AnnotationValue that will be the DeclaredType of the original class.

This sounds much more complicated than just calling a method. But don’t be too scared: only a few dozen lines of code would be needed for the whole plan.

Let’s start with the first stage of the plan.

Stage 1. Finding the Dependencies method

Remember that not all Components are required to have Component Dependencies. So it’s okay if some Component doesn’t have a declared dependencies method. We should immediately take into account the possibility that we might receive null.

Then we’ll proceed with the plan: we get all annotations via annotationMirrors (1), find the Component annotation among them (2), and then find the dependencies method (3) from which we will attempt to get the value (4).

private fun getDependenciesMethod(
componentNode: BindingGraph.ComponentNode
)
: AnnotationValue? {
val component = componentNode.componentPath().currentComponent()
val annotationMirrors = component.annotationMirrors // (1)
val componentAnnotationMirror = annotationMirrors // (2)
.first { it.annotationType.toString() == Component::class.java.name }
val dependenciesMethod = componentAnnotationMirror.elementValues.entries // (3)
.firstOrNull { it.key.simpleName.toString() == Component::dependencies.name }
return dependenciesMethod?.value // (4)
}

Cool! We have the value of the dependencies field. Now we move on to the second stage of the plan.

Stage 2. Getting the dependencies types

We know for sure that the value of the dependencies field in the code is an array of classes, which in the language of annotationMirror means List<AnnotationValue>. So we can boldly cast it (1) and extract the AnnotationValue value (2), the type of which we also know. Therefore, we can very boldly cast the values to DeclaredType again (3).

Btw, we must not forget that a component may not have any Component Dependencies. Hence, don’t forget to add a check for null.

private fun getComponentDependencies(
componentNode: BindingGraph.ComponentNode
)
: List<DeclaredType> {
val dependenciesMethod = getDependenciesMethod(componentNode)

return if (dependenciesMethod != null) {
(dependenciesMethod.value as List<AnnotationValue>) // (1)
.map { it.value } // (2)
.map { it as DeclaredType } // (3)
} else {
emptyList()
}
}

Finally! We got a list of our Component Dependencies. The problem with MirroredTypeException has been solved!

Let’s move on, we now need to extract all the dependencies in a convenient format.

Getting all types

To do this, we will work with each of the Component Dependencies in turn. We’ll find all the methods of Component Dependency (1) and get the type of value they return (2).

private fun getMethodsReturnTypes(declaredType: DeclaredType): List<String> {
val methods = declaredType.asElement().enclosedElements // (1)
.filter { it.kind == ElementKind.METHOD }
.map { it as ExecutableElement }
val returnTypes = methods // (2)
.map { it.returnType.toString() }
return returnTypes
}

Now let’s put it all together. We find the types of Component Dependencies and assemble them into a dictionary from Component Dependency and the list of types (dependencies) it provides.

private fun getDependencies(
componentNode: BindingGraph.ComponentNode
)
: Map<String, List<String>> {
val dependenciesTypes = getComponentDependencies(componentNode)
val dependenciesMap = dependenciesTypes
.associateWith { getMethodsReturnTypes(it) }
.mapKeys { it.key.toString() }
return dependenciesMap
}

The hardest part is over. We have a list of dependencies that can provide Component Dependencies specified in Component. The next thing we need to do is get the list of dependencies that our Component requires.

Extracting the list of required dependencies

It’s surprisingly simple. Through the bindings method, Dagger SPI provides us with a separate list of used dependencies.

However, it will return them in its own format. But we need to extract them because we have a need for the type names.

val bindings = bindingGraph.bindings()
.map { contextBinding -> contextBinding.key().type().toString() }

That’s awesome! We have the list of required dependencies. It remains to compare it with what a particular Component Dependency provides us with.

Performing the search

Let’s go through the list of Component Dependencies and see how many of the dependencies it provides are not used. If all of them, we can boldly declare such Component Dependency as unnecessary. Because we know for sure that none of its methods were useful, which means it is unused and can be removed.

dependencies.forEach { (dependency, dependencyMethods) ->
val unusedMethods = dependencyMethods.subtract(bindings)
if (unusedMethods.size == dependencyMethods.size) {
diagnosticReporter.reportComponent(
Diagnostic.Kind.ERROR,
currentComponent,
"Dependency ${dependency} is unused"
)
}
}

That’s it. Let’s try to build the project, and at the code generation stage, Dagger will give us the expected error.

SomeComponent.java:8: error: 
[ru.cian.validate.dagger.deps.unused.DaggerUnusedValidator]
Dependency CoreNetworkDependencies is unused

If you’re interested in the full code, it can be found in Gist.

As far as our project is concerned, it turned out that there were many forgotten Component Dependencies. With a removal of all unused dependencies, we have reduced the number of unnecessary links between modules. The Dependency Analysis Gradle Plugin helped us with this, for which we are grateful.

Most of the unused Component Dependencies were found in old features that were 3 or more years old, while new features had almost no unused Component Dependencies.

What else can be done with SPI?

You will probably think that finding unused Component Dependencies is certainly fun, interesting, useful, and overall quite noble, but dragging in Dagger SPI for the sake of one feature is like overkill.

That’s pretty fair. So I took the liberty of presenting what else Dagger SPI can be used for. The following options came to mind off the top of my head:

Analytics

  • Visualization and analysis of the dependency graph. Dagger SPI is used by most Dagger graph visualizers. The visualization of the dependency graph is pleasing to the eye, but even more useful might be the ability to save the dependency graph to a separate file. This allows for more sophisticated analysis of the graph using an external script.
  • Dagger health metrics. You can collect data on how well Dagger is being used in your project. For example, the number of all dependencies of a particular Component. If there are too many of them, it is a reason to create a refactoring task.

Validation

  • Using Qualifier. It’s possible to require developers to use Qualifier for basic types, such as Int or String. This will save you from problems where you wanted to get one string but ended up with a completely different one.
  • Prohibition of use. You can prohibit providing classes that are prone to causing memory leaks directly to Dagger, such as Activity, Fragment, View, etc.
  • Finding unnecessary provides. You can find all the provide-methods that provide dependencies that no one needs and delete them after that

Log improvement

  • Dagger doesn’t always produce errors that are easy to understand for a beginner. Nothing prevents you from adding explanations to the log if you find them, detailing how to fix them specifically in your project.

That’s all for now, but I think there are many more possibilities to explore.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

No responses yet

Write a response