Modularizing a project is never easy, a lot of files to move and the dependencies between them is not always what we expect. Then the Dagger configuration used in a single module project often doesn't scale well to a multi module project. Hilt is opinionated about the configuration to use (we don't need to argue anymore about using component dependencies or subcomponents!) and this configuration works perfectly even in a multi module project. In this talk we'll see first an introduction to Hilt and a comparison with Dagger to understand why it's easier to configure. Then we'll see how to leverage it in a multi module project (both in a standard layered architecture and in a Clean Architecture that uses the Dependency Inversion) to improve build speed and code testability. Spoiler alert: using sample apps that include a single feature in the app helps a lot!
8. class MyClass {
private val collaborator2 = Collaborator2()
fun execute() {
val value = Collaborator1.loadSomething()
collaborator2.doSomethingElse(value)
}
}
Plaincode
9. class MyClass(serviceLocator: ServiceLocator) {
private val collaborator1 = serviceLocator.collaborator1
private val collaborator2 = serviceLocator.collaborator2
fun execute() {
val value = collaborator1.loadSomething()
collaborator2.doSomethingElse(value)
}
}
ServiceLocator
10. class MyClass(
private val collaborator1: Collaborator1,
private val collaborator2: Collaborator2
) {
fun execute() {
val value = collaborator1.loadSomething()
collaborator2.doSomethingElse(value)
}
}
DependencyInjection
11. class MyClass(
private val collaborator1: Collaborator1,
private val collaborator2: Collaborator2
) {
fun execute() {
val value = collaborator1.loadSomething()
collaborator2.doSomethingElse(value)
}
}
class MyClass(serviceLocator: ServiceLocator) {
private val collaborator1 = serviceLocator.collaborator1
private val collaborator2 = serviceLocator.collaborator2
fun execute() {
val value = collaborator1.loadSomething()
collaborator2.doSomethingElse(value)
}
}
ServiceLocatorDependencyInjection
Dependencies are
retrieved using the
Service Locator
Dependencies are
injected by the
container
12. Dagger is a Dependency Injection framework
But a Dagger component can be used as a Service Locator
13. Is Dagger a Dependency Injection framework?
What about Hilt?
22. @Module
object MyModule {
@Provides
@Singleton
fun provideApi(): Api {
return Retrofit.Builder()
.baseUrl("""...")
!//!!...
.build()
.create(Api"::class.java)
}
}
Using an object
instead of a class
Dagger generates
less code
24. @Singleton
class MyRepository @Inject constructor(
private val cache: Cache,
private val api: Api
) {
fun loadData() = cache.load() "?: api.load()
}
25. interface MyRepository {
fun loadData(): Any
}
@Singleton
class MyRepositoryImpl @Inject constructor(
private val cache: Cache,
private val api: Api
) : MyRepository {
override fun loadData() = cache.load() "?: api.load()
}1
26. interface MyRepository {
fun loadData(): Any
}
@Singleton
class MyRepositoryImpl @Inject constructor(
private val cache: Cache,
private val api: Api
) : MyRepository {
override fun loadData() = cache.load() "?: api.load()
}1
@Module
interface AnotherModule {
@Binds
fun MyRepositoryImpl.bindsRepository(): MyRepository
}
36. class MainActivity : AppCompatActivity() {
@Inject
lateinit var permissionManager: PermissionManager
@Inject
lateinit var mainNavigator: MainNavigator
!//!!...
}
37. @AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var permissionManager: PermissionManager
@Inject
lateinit var mainNavigator: MainNavigator
!//!!...
}
38.
39. @AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var permissionManager: PermissionManager
@Inject
lateinit var mainNavigator: MainNavigator
!//!!...
}
40. @AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val viewModel: MyViewModel by viewModels()
@Inject
lateinit var permissionManager: PermissionManager
@Inject
lateinit var mainNavigator: MainNavigator
!//!!...
}
48. !!/**
* A generated base class to be extended by the @dagger.hilt.android.AndroidEntryPoint annotated class.
* If using the Gradle plugin, this is swapped as the base class via bytecode transformation.
!*/
public abstract class Hilt_MainActivity extends AppCompatActivity
implements GeneratedComponentManagerHolder {
"//""...
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
((MainActivity_GeneratedInjector) this.generatedComponent())
.injectMainActivity(UnsafeCasts.<MainActivity>unsafeCast(this));
super.onCreate(savedInstanceState);
}
"//""...
}
49. !!/**
* A generated base class to be extended by the @dagger.hilt.android.AndroidEntryPoint annotated class.
* If using the Gradle plugin, this is swapped as the base class via bytecode transformation.
!*/
public abstract class Hilt_MainActivity extends AppCompatActivity
implements GeneratedComponentManagerHolder {
"//""...
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
((MainActivity_GeneratedInjector) this.generatedComponent())
.injectMainActivity(UnsafeCasts.<MainActivity>unsafeCast(this));
super.onCreate(savedInstanceState);
}
"//""...
}
50. !!/**
* A generated base class to be extended by the @dagger.hilt.android.AndroidEntryPoint annotated class.
* If using the Gradle plugin, this is swapped as the base class via bytecode transformation.
!*/
public abstract class Hilt_MainActivity extends AppCompatActivity
implements GeneratedComponentManagerHolder {
"//""...
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
((MainActivity_GeneratedInjector) this.generatedComponent())
.injectMainActivity(UnsafeCasts.<MainActivity>unsafeCast(this));
super.onCreate(savedInstanceState);
}
"//""...
}
57. class MyClass @Inject constructor(
private val collaborator1: Collaborator1,
private val collaborator2: Collaborator2
) {
fun execute() {
val value = collaborator1.loadSomething()
collaborator2.doSomethingElse(value)
}
}
58. class MyClassTest {
private val collaborator1 = mock<Collaborator1>()
private val collaborator2 = mock<Collaborator2>()
private val myObject = MyClass(collaborator1, collaborator2)
@Test
fun testSomething() {
whenever(collaborator1.loadSomething()) doReturn "something"
myObject.execute()
verify(collaborator2).doSomethingElse("something")
}
}
59.
60. @Singleton
open class MyAnalytics @Inject constructor()
interface MyUseCase
@Singleton
class MyUseCaseImpl @Inject constructor() : MyUseCase
@Module
@InstallIn(SingletonComponent"::class)
interface MyModule {
@Binds
fun MyUseCaseImpl.bindsUseCase(): MyUseCase
}
@AndroidEntryPoint
class MyActivity : AppCompatActivity() {
@Inject
lateinit var analytics: MyAnalytics
@Inject
lateinit var useCase: MyUseCase
!//!!...
}
62. @HiltAndroidTest
class MyActivityTest {
@get:Rule1
val hiltRule = HiltAndroidRule(this)
@get:Rule
val rule = ActivityTestRule(MyActivity"::class.java, false, false)
@Inject
lateinit var analytics: MyAnalytics
@Inject
lateinit var useCase: MyUseCase
@Test
fun startActivity() {
rule.launchActivity(null)
hiltRule.inject()
!//now the properties contain the production objects
}
}
63. @HiltAndroidTest
class MyActivityTest {
@get:Rule1
val hiltRule = HiltAndroidRule(this)
@get:Rule
val rule = ActivityTestRule(MyActivity"::class.java, false, false)
@Test
fun startActivity() {
rule.launchActivity(null)
!//!!...
}
}
64. @Module
@InstallIn(SingletonComponent"::class)
object FakeAnalyticsModule {
@Provides
fun provideAnalytics(): MyAnalytics = mock()
}
@HiltAndroidTest
class MyActivityTest {
@get:Rule1
val hiltRule = HiltAndroidRule(this)
@get:Rule
val rule = ActivityTestRule(MyActivity"::class.java, false, false)
@Test
fun startActivity() {
rule.launchActivity(null)
!//!!...
}
}
65. @HiltAndroidTest
class MyActivityTest {
@get:Rule1
val hiltRule = HiltAndroidRule(this)
@get:Rule
val rule = ActivityTestRule(MyActivity"::class.java, false, false)
@Module
@InstallIn(SingletonComponent"::class)
object FakeAnalyticsModule {
@Provides
fun provideAnalytics(): MyAnalytics = mock()
}
@Test
fun startActivity() {
rule.launchActivity(null)
!//!!...
}
}
66. @HiltAndroidTest
class MyActivityTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)
@get:Rule
val rule = ActivityTestRule(MyActivity"::class.java, false, false)
@BindValue
@JvmField
val analytics: MyAnalytics = mock()
@Test
fun startActivity() {
rule.launchActivity(null)
!//!!...
}
}
67. @HiltAndroidTest
class MyActivityTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)
@get:Rule
val rule = ActivityTestRule(MyActivity"::class.java, false, false)
@BindValue
@JvmField
val useCase: MyUseCase = mock()
@Test
fun startActivity() {
rule.launchActivity(null)
!//!!...
}
}
error: [Dagger/DuplicateBindings] MyUseCase is bound multiple times
86. Wrappingup
Dependency Injection on classes we can instantiate
@Inject, @Provides and @Binds
Easy setup on classes instantiated by the framework
@AndroidEntryPoint and @HiltAndroidApp
Testability
@HiltAndroidTest and HiltAndroidRule