3

Tomáš Mlynarič

 3 years ago
source link: https://proandroiddev.com/lock-your-dagger-in-gradle-modules-e4270d61e138
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Sharpening Dagger

Lock your Dagger in Gradle Modules

Why encapsulate Dagger dependencies in Gradle modules

The time has come. You've finally decided to split your monolithic Gradle module into smaller ones. But have you considered splitting your almighty Dagger AppComponent as well? Everything works, so why bother, right?

Note: In the article, references to modules refer to Gradle modules and not to Dagger modules.

Dependency encapsulation (api vs implementation)

Imagine you have 3 modules :app, :feature and :library as in the picture below.

1*0lg7BF5-VwilQw0uvE6ymw.png?q=20
lock-your-dagger-in-gradle-modules-e4270d61e138

Your :app module depends on the :feature module in the usual way

implementation(project(":feature"))

and the :feature module depends on some external :library

implementation(some.library:library:1.0.0) 

Note: You may have a similar setup if you follow layer-based modularization.

You chose the :library because it contains some awesome LibraryManager you want to use in your codebase. Suddenly, your inner voice reminded you of the Dependency inversion principle, so you encapsulated the LibraryManager into your own class FeatureRepository. Right after you stopped writing, the inner voice whispered again. “This time check Inversion of Control,” it said. And so you added @Inject to the constructor of the FeatureRepository.

Since there was no more whispering, you compiled the project 🥁 … and it failed.

LibraryManager cannot be provided without an @Inject constructor or an @Provides-annotated method.

Oh yeah, you forgot — Dagger doesn’t know anything about LibraryManager.

You need to create Dagger @Module inside your :feature Gradle module which @Provides the LibraryManager.

Now it should be fine, right? You don’t have any import from the library in :app module — only your FeatureRepository .

You compile again … and it fails, again.

DaggerAppComponent.java:26: error: cannot access LibraryManager
0*rH30F-jwMYVX5t5r.jpg?q=20
lock-your-dagger-in-gradle-modules-e4270d61e138

I'll spare your time chasing the error reason. The problem is that Dagger generates the code composition only in a class implementing @Component interface. The code is generated within the :app module and attempts to instantiate LibraryManager from the :library. It doesn't “see” it though, because of Gradle's implementation(:library).

The same would happen if you wanted to access the class manually from your :app module. But in this case, IDE error inspection would give you a hint.

The picture below explains what happens graphically.

1*s4J8lAhfQtW8gsxXAPhP4Q.png?q=20
lock-your-dagger-in-gradle-modules-e4270d61e138

What can you do about it?
The simplest solution is to change implementationto api in Gradle file and it's done 🎤 ⤵️. But please, don't 🙏 . It breaks encapsulation — making FeatureManager visible to all of your modules! Also, each time :library ABI changes, Gradle will recompile each module depending on the :feature module!

The proper solution is a bit harder than a one-line fix. I’ll show you how to do it — but first, I’d like to throw more wood into the fire by describing a similar situation: Encapsulating your own code.

Implementation encapsulation (public vs internal)

Imagine the same project with :app and :feature modules (we'll forget about :library for this example). You want your FeatureRepository to be public interface and implementation FeatureRepositoryImpl to be internal, so that nothing but the :feature module can access its guts.

The picture and code below represent the situation:

1*uk-ktLXYjRXZbAGzDwYwaA.png?q=20
lock-your-dagger-in-gradle-modules-e4270d61e138

In this case, you don’t have to explicitly use @Provides for your FeatureRepository, but you need to tell Dagger the relation between implementation and its interface with @Binds (@Provides would also work, but it's less performant).

As you’ve guessed by now, you will face the same problem as before. But this time, even the IDE error inspection tells you that you can’t do that:

`public` function exposes its `internal` parameter type FeatureRepositoryImpl

What can you do about it?
Again, the simplest solution is to make FeatureRepositoryImpl as public. The question then is whether you get any benefit from implementing the interface at all. Also, if you wanted to hide some different implementation of the same interface, you can't. It has to be public.

Encapsulate Dagger in Gradle module

To encapsulate dependencies in Gradle modules, you need to create a separate Dagger @Component in each module. You can't create @Subcomponent for this, because the subcomponent generates code within its parent component (and therefore still in the :app).

Technically, encapsulating internal implementations would work with @Subcomponent. Java doesn't have internal visibility modifier and therefore classes compiled from Kotlin are public. But if you want to encapsulate a 3rd-party dependency, the problem will be the same — it won't compile.

To kill two birds with one stone, we have to stick to component dependencies.

First, we need to create FeatureComponent and explicitly specify what it can provide outside of the component — this is Dagger's kind of public/private visibility modifier:

Then, add the component to dependencies list in AppComponent:

And finally, instantiate both components in your Application and pass FeatureComponent to AppComponent's factory method.

You can now enjoy encapsulated dependencies 🎉. You no longer have to worry about someone using an implementation instead of an abstraction. Nor do you need to worry about polluting your codebase with some library code.

This setup allows both examples from the beginning of the article to work.

Need Scope?

There’s one catch though (there always is, right? 😅). If you want to have your FeatureComponent scoped (e.g. @FeatureScope), the compilation will fail again.

AppComponent.java:7: error: This @Singleton component cannot depend on scoped components: @FeatureScope FeatureComponent

This looks like a dead-end, right? Well, not entirely.

Dagger component is a simple annotated interface (or abstract class) with generated implementation. We can trick Dagger compiler to skip the scope checking by passing dependency to a “contract” instead of the real component.

It works like this: AppComponent depends on FeatureComponentContract, which is implemented by the FeatureComponent. This way, AppComponent doesn't depend directly on the FeatureComponent. Since the contract is just a plain interface, Dagger won’t complain about scopes. At the same time, it will honor the annotations of the components and will generate their implementations.

However, we will have to move the featureRepository: FeatureRepository definition to the contract interface. This way, AppComponent knows that it can get the featureRepository from the contract and doesn't care about what implements the contract.

There won't be any change in your Application class, because everything accepts the same interface.

Visually, it looks like this:

1*u2injsTmf8ioxgjef0TJ_Q.png?q=20
lock-your-dagger-in-gradle-modules-e4270d61e138

Code representation of FeatureComponent with FeatureComponentContract looks like this:

and finally, ApplicationComponent:

This way, you can have Dagger component in each Gradle module and properly encapsulate your code. Each of your components can hold “local singletons.” Your FeatureComponent can hold objects annotated with @FeatureScope, and AppComponent can hold @Singleton.

There's one trick for those who’ve just started to modularize their projects and have @Singleton across the whole codebase. You can have the FeatureComponent annotated with @Singleton instead of a custom scope. Be careful, though! This may result in your app containing multiple instances of the same class annotated with @Singleton, because each scoped class is held in its component. If you have some @Module which @Provides the same @Singleton object inside of multiple components, it will be held as a singleton in each of those components.

Conclusion

Nowadays, many apps have multiple Gradle modules — but stick to one Dagger component. If you want more control over the visibility of your dependencies, you should consider locking Dagger component in each Gradle module.

PS: This setup is also great for dynamic feature modules, but more about that next time 🤫.

Thanks

and for reviewing the article.

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK