Clean Architecture and app modularization are often used together to achieve a better code structure and a faster build time. But how can we use Dagger in an app structured in that way? Can we use subcomponents (with or without Dagger Android) or are component dependencies enough?
In this talk we’ll see how to leverage Dagger to organize the dependencies in a multi-module project with particular attention to testing and decoupling. The examples will be both in a standard layered architecture and in a Clean Architecture where the Dependency Inversion increases the overall structure but can complicate the Dagger code.
7. 1. Speeds up builds
2. Enable on demand delivery
3. Simplify development
4. Reuse modules across apps
5. Experiment with new technologies
6. Scale development teams
7. Enables refactoring
8. Simplifies test automation
Modularization - Why you should care
https://jeroenmols.com/blog/2019/03/06/modularizationwhy/
31. DaggerTip#1
Exception in thread "main" java.lang.IllegalStateException: Component1 must be set
at dagger.internal.Preconditions.checkBuilderRequirement(Preconditions.java:95)
at DaggerComponent2$Builder.build(DaggerComponent2.java:35)
@Component
interface Component1
@Component(dependencies = [Component1::class])
interface Component2
fun main() {
DaggerComponent2.builder()
.build()
}1
32. DaggerTip#1
Exception in thread "main" java.lang.IllegalStateException: Component1 must be set
at dagger.internal.Preconditions.checkBuilderRequirement(Preconditions.java:95)
at DaggerComponent2$Builder.build(DaggerComponent2.java:35)
@Component
interface Component1
@Component(dependencies = [Component1::class])
interface Component2
fun main() {
DaggerComponent2.builder()
.component1(DaggerComponent1.create())
.build()
}1
35. DaggerTip#2
@Module
class Module1 {
@Provides
fun pattern() = "#,###.00"
@Provides
fun format(pattern: String) =
DecimalFormat(pattern)
}1
@Component(modules = [Module1::class])
interface Component1 {
val format: DecimalFormat
}2
Module1
Component1
pattern
format
format
36. DaggerTip#2 @Module
class Module1 {
@Provides
fun pattern() = "#,###.00"
@Provides
fun format(pattern: String) =
DecimalFormat(pattern)
}1
@Component(modules = [Module1::class])
interface Component1 {
val format: DecimalFormat
}2
@Module
class Module2 {
}3
@Component(
modules = [Module2::class],
dependencies = [Component1::class]
)5
interface Component2 {
}4
Module2
Component2
Module1
Component1
pattern
format
format
37. DaggerTip#2 @Module
class Module1 {
@Provides
fun pattern() = "#,###.00"
}1
@Component(modules = [Module1::class])
interface Component1 {
}2
@Module
class Module2 {
@Provides
fun format(pattern: String) =
DecimalFormat(pattern)
}3
@Component(
modules = [Module2::class],
dependencies = [Component1::class]
)5
interface Component2 {
val format: DecimalFormat
}4
Module2
Component2
format
Module1
Component1
pattern
format
error: [Dagger/MissingBinding] String cannot be provided without an @Inject constructor or
an @Provides-annotated method.
public abstract interface Component2 {
^
String is injected at
Module2.format(pattern)
DecimalFormat is provided at
Component2.getFormat()
38. DaggerTip#2 @Module
class Module1 {
@Provides
fun pattern() = "#,###.00"
}1
@Component(modules = [Module1::class])
interface Component1 {
val pattern: String
}2
@Module
class Module2 {
@Provides
fun format(pattern: String) =
DecimalFormat(pattern)
}3
@Component(
modules = [Module2::class],
dependencies = [Component1::class]
)5
interface Component2 {
val format: DecimalFormat
}4
Module2
Component2
format
Module1
Component1
patternpattern
format
39. DaggerTip#2 @Module
class Module1 {
@Provides
fun pattern() = "#,###.00"
}1
@Component(modules = [Module1::class])
interface Component1 {
val pattern: String
}2
@Module
class Module2 {
@Provides
fun format(pattern: String) =
DecimalFormat(pattern)
}3
@Component(
modules = [Module2::class],
dependencies = [Component1::class]
)5
interface Component2 {
val format: DecimalFormat
}4
Module2
Component2
format
Module1
Component1
patternpattern
format
40. DaggerTip#3
interface Component1 {
val pattern: String
}2
@Module
class Module2 {
@Provides
fun format(pattern: String) =
DecimalFormat(pattern)
}3
@Component(
modules = [Module2::class],
dependencies = [Component1::class]
)5
interface Component2 {
val format: DecimalFormat
}4
41. DaggerTip#3 interface Component1 {
val pattern: String
}2
@Module
class Module2 {
@Provides
fun format(pattern: String) =
DecimalFormat(pattern)
}3
@Component(
modules = [Module2::class],
dependencies = [Component1::class]
)5
interface Component2 {
val format: DecimalFormat
}4
fun main() {
val component1 = object : Component1 {
override val pattern ="#,###.0000"
}3
val component2 = DaggerComponent2.factory().create(component1)
}4
42. @Module
class LibModule {
@Provides
fun provideMyLibObject() = MyLibObject()
}3
@Component(modules = [LibModule::class])
interface LibComponent {
val myLibObject: MyLibObject
}1
Lib
43. @Module
class LibModule {
@Provides
fun provideMyLibObject() = MyLibObject()
}3
@Component(modules = [LibModule::class])
interface LibComponent {
val myLibObject: MyLibObject
}1
Lib
feature2
Lib
feature1
51. interface Feature1Provider {
val feature1Component: Feature1Component
}7
interface LibComponentProvider {
val libComponent: LibComponent
}8
feature1Lib
52. class MyApp : Application(), LibComponentProvider, Feature1Provider {
override val libComponent =
DaggerLibComponent.create()
override val feature1Component =
DaggerFeature1Component.factory().create(libComponent)
}6
interface Feature1Provider {
val feature1Component: Feature1Component
}7
interface LibComponentProvider {
val libComponent: LibComponent
}8
feature1LibApp
53. class MyApp : Application(), LibComponentProvider, Feature1Provider {
override val libComponent =
DaggerLibComponent.create()
override val feature1Component =
DaggerFeature1Component.factory().create(libComponent)
}6
interface Feature1Provider {
val feature1Component: Feature1Component
}7
fun Application.feature1Component() =
(this as Feature1Provider).feature1Component
interface LibComponentProvider {
val libComponent: LibComponent
}8
fun Application.libComponent() =
(this as LibComponentProvider).libComponent
Libfeature1App
54. class MyApp : Application(), LibComponentProvider, Feature1Provider {
override val libComponent =
DaggerLibComponent.create()
override val feature1Component =
DaggerFeature1Component.factory().create(libComponent)
}6
class MyFeature1App : Application(), LibComponentProvider, Feature1Provider {
override val libComponent =
DaggerLibComponent.create()
override val feature1Component =
DaggerFeature1Component.factory().create(libComponent)
}3
interface Feature1Provider {
val feature1Component: Feature1Component
}7
fun Application.feature1Component() =
(this as Feature1Provider).feature1Component
interface LibComponentProvider {
val libComponent: LibComponent
}8
fun Application.libComponent() =
(this as LibComponentProvider).libComponent
Libfeature1Feature1AppApp
55. interface ComponentHolder {
val map: MutableMap<KClass<*>, Any>
}1
open class ComponentHolderApp : Application(), ComponentHolder {
override val map = mutableMapOf<KClass<*>, Any>()
}2
inline fun <reified C : Any> ComponentHolder.getOrCreate(factory: () -> C): C =
map.getOrPut(C::class, factory) as C
A “real” implementation is available here:
https://github.com/fabioCollini/CleanWeather/blob/dagger/kotlinUtils/
src/main/java/it/codingjam/cleanweather/kotlinutils/ComponentHolder.kt
56. class MyApp : Application(), LibComponentProvider, Feature1Provider {
override val libComponent =
DaggerLibComponent.create()
override val feature1Component =
DaggerFeature1Component.factory().create(libComponent)
}6
class MyFeature1App : Application(), LibComponentProvider, Feature1Provider {
override val libComponent =
DaggerLibComponent.create()
override val feature1Component =
DaggerFeature1Component.factory().create(libComponent)
}3
interface Feature1Provider {
val feature1Component: Feature1Component
}7
fun Application.feature1Component() =
(this as Feature1Provider).feature1Component
interface LibComponentProvider {
val libComponent: LibComponent
}8
fun Application.libComponent() =
(this as LibComponentProvider).libComponent
Libfeature1Feature1AppApp
57. class MyApp : ComponentHolderApp(), LibComponentProvider, Feature1Provider {
override val libComponent =
DaggerLibComponent.create()
override val feature1Component =
DaggerFeature1Component.factory().create(libComponent)
}6
class MyFeature1App : ComponentHolderApp(), LibComponentProvider, Feature1Provider {
override val libComponent =
DaggerLibComponent.create()
override val feature1Component =
DaggerFeature1Component.factory().create(libComponent)
}3
interface Feature1Provider {
val feature1Component: Feature1Component
}7
fun ComponentHolderApp.feature1Component() =
(this as Feature1Provider).feature1Component
interface LibComponentProvider {
val libComponent: LibComponent
}8
fun ComponentHolderApp.libComponent() =
(this as LibComponentProvider).libComponent
Libfeature1Feature1AppApp
58. class MyApp : ComponentHolderApp(), LibComponentProvider, Feature1Provider {
override val libComponent =
DaggerLibComponent.create()
override val feature1Component =
DaggerFeature1Component.factory().create(libComponent)
}6
class MyFeature1App : ComponentHolderApp(), LibComponentProvider, Feature1Provider {
override val libComponent =
DaggerLibComponent.create()
override val feature1Component =
DaggerFeature1Component.factory().create(libComponent)
}3
interface Feature1Provider {
val feature1Component: Feature1Component
}7
fun ComponentHolderApp.feature1Component() = getOrCreate {
}1
interface LibComponentProvider {
val libComponent: LibComponent
}8
fun ComponentHolderApp.libComponent() = getOrCreate {
}2
Libfeature1Feature1AppApp
59. class MyApp : ComponentHolderApp(), LibComponentProvider, Feature1Provider
class MyFeature1App : ComponentHolderApp(), LibComponentProvider, Feature1Provider
interface Feature1Provider {
val feature1Component: Feature1Component
}7
fun ComponentHolderApp.feature1Component() = getOrCreate {
DaggerFeature1Component.factory()
.create(libComponent())
}1
interface LibComponentProvider {
val libComponent: LibComponent
}8
fun ComponentHolderApp.libComponent() = getOrCreate {
DaggerLibComponent.create()
}2
Feature1App
App Libfeature1
60. class MyApp : ComponentHolderApp()
class MyFeature1App : ComponentHolderApp()
fun ComponentHolderApp.feature1Component() = getOrCreate {
DaggerFeature1Component.factory()
.create(libComponent())
}1
fun ComponentHolderApp.libComponent() = getOrCreate {
DaggerLibComponent.create()
}2
Libfeature1
Feature1App
App
61. @Feature1SingletonScope
@Component(dependencies = [Lib1Component::class, Lib2Component::class])
interface Feature1Component {
//...
}
DaggerTip#5
error: @Feature1SingletonScope Feature1Component depends on more than one scoped component:
@dagger.Component(dependencies = {Lib1Component.class, Lib2Component.class})
^
@Lib1SingletonScope Lib1Component
@Lib2SingletonScope Lib2Component
91. class MainActivityTest {
@get:Rule
val rule = ActivityTestRule(MainActivity::class.java, false, false)
private val useCaseMock = mock<UseCase>()
@Before
fun setUp() {
}2
}4
92. class MainActivityTest {
@get:Rule
val rule = ActivityTestRule(MainActivity::class.java, false, false)
private val useCaseMock = mock<UseCase>()
@Before
fun setUp() {
val app = ApplicationProvider.getApplicationContext<ComponentHolderApp>()
app.map.clear()
app.map[DomainComponent::class] = object : DomainComponent {
override val useCase = useCaseMock
}1
}2
}4
93. class MainActivityTest {
@get:Rule
val rule = ActivityTestRule(MainActivity::class.java, false, false)
private val useCaseMock = mock<UseCase>()
@Before
fun setUp() {
val app = ApplicationProvider.getApplicationContext<ComponentHolderApp>()
app.map.clear()
app.map[DomainComponent::class] = object : DomainComponent {
override val useCase = useCaseMock
}1
}2
@Test
fun launchActivity() {
whenever(useCaseMock.retrieve()) doReturn "ABCDEF"
rule.launchActivity(null)
onView(withId(R.id.text))
.check(matches(withText("ABCDEF")))
}3
}4
94. class MainActivityTest {
@get:Rule
val rule = ActivityTestRule(MainActivity::class.java, false, false)
private val useCaseMock = mock<UseCase>()
@Before
fun setUp() {
val app = ApplicationProvider.getApplicationContext<ComponentHolderApp>()
app.map.clear()
app.map[DomainComponent::class] = object : DomainComponent {
override val useCase = useCaseMock
}1
}2
@Test
fun launchActivity() {
whenever(useCaseMock.retrieve()) doReturn "ABCDEF"
rule.launchActivity(null)
onView(withId(R.id.text))
.check(matches(withText("ABCDEF")))
}3
}4
95. Wrappingup
Component dependencies
Each module exposes:
a Component interface
a method to create the Component
A map in the App can be useful to:
manage singletons
replace real objects with fakes/mocks
96. Links
Demo Project
github.com/fabioCollini/CleanWeather/tree/dagger
Droidcon Italy talk - SOLID principles in practice: the Clean Architecture
youtube.com/watch?v=GlDsfq3xHvo&t=
Implementing Dependency Inversion using Dagger components
medium.com/google-developer-experts/implementing-dependency-inversion-using-dagger-components-d6b0fb3b6b5e
Inversion library
github.com/fabioCollini/Inversion
Yigit Boyar, Florina Muntenescu - Build a Modular Android App Architecture (Google I/O'19)
youtube.com/watch?v=PZBg5DIzNww
Jeroen Mols - Modularization - Why you should care
jeroenmols.com/blog/2019/03/06/modularizationwhy/
Ben Weiss - Dependency injection in a multi module project
medium.com/androiddevelopers/dependency-injection-in-a-multi-module-project-1a09511c14b7
Marcos Holgado - Using Dagger in a multi-module project
proandroiddev.com/using-dagger-in-a-multi-module-project-1e6af8f06ffc