Search code examples
androidkotlindependency-injectionclean-architectureandroid-architecture

How Can I Integrate Dagger in the Domain Layer and Hilt in the App and Data Layers of an Android Application following CLEAN architecture principles?


I am new to Android development and currently delving into app architecture, specifically the separation of concerns across different layers (domain, data, and app). My understanding is that the domain layer should be independent of the Android framework and exist as a pure Kotlin library. Consequently, I should not use Hilt within this layer but opt for Dagger instead.

In my setup, the domain layer defines a repository interface and a use case interface along with its implementation. The data layer is responsible for implementing the repository, and the app layer manages the activities and the Hilt application setup. My goal is to leverage Dagger for dependency injection in the domain layer, while using Hilt in both the app and data layers.

However, I've encountered a compilation error when attempting to inject a use case into an activity.

The error is as follows:

app/build/generated/hilt/component_sources/debug/application/MyApp_HiltComponents.java:137: error: [Dagger/MissingBinding] application.domain.usecases.MyUseCase cannot be provided without an @Provides-annotated method.
  public abstract static class SingletonC implements FragmentGetContextFix.FragmentGetContextFixEntryPoint,
                         ^
  
  Missing binding usage:
      application.domain.usecases.MyUseCase is injected at
          application.ui.MainActivity.myUseCase
      application.ui.MainActivity is injected at
          application.ui.MainActivity_GeneratedInjector.injectMainActivity(application.ui.MainActivity) [application.MyApp_HiltComponents.SingletonC → application.MyApp_HiltComponents.ActivityRetainedC → application.MyApp_HiltComponents.ActivityC]

And here is a simplified version of my code:

Domain layer (Dagger, no Hilt):

interface MyRepository {
    suspend fun findAllUser(): Flow<List<User>>
}

interface MyUseCase {
    suspend fun findAllUserRandom(): Flow<List<User>>
}

class MyUseCaseImpl @Inject constructor(
    private val myRepository: MyRepository,
): MyUseCase {
    override suspend fun findAllUserRandom(): Flow<List<User>> {
        return myRepository.findAll().map { list ->
            list.filter { 
                Random.nextInt() % 2 == 0
            }
        }
    }
}

@Module
class DomainModule {
    @Provides
    @Singleton
    fun provideMyUseCase(myRepository: MyRepository): MyUseCase {
        return MyUseCaseImpl(myRepository)
    }
}

Data layer (with hilt):

class MyRepositoryImpl @Inject constructor(
    private val myRoomDao: MyRoomDao,
): MyRepository {
    override suspend fun findAllUser(): Flow<List<User>> {
        return myRoomDao.findAll()
    }
}

@Module
@InstallIn(SingletonComponent::class)
interface DataModule {

    companion object {
        @Singleton
        @Provides
        fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase {
            return Room.databaseBuilder(
                context,
                AppDatabase::class.java,
                "my_database"
            ).build()
        }

        @Singleton
        @Provides
        fun provideMyRoomDao(appDatabase: AppDatabase): MyRoomDao {
            return appDatabase.myRoomDao()
        }
    }

    @Binds
    @Singleton
    fun bindMyRepository(myRepositoryImpl: MyRepositoryImpl): MyRepository
}

App layer:

@HiltAndroidApp
class MyApp : Application()

@AndroidEntryPoint
class MainActivity : ComponentActivity() {

    @Inject
    lateinit var myUseCase: MyUseCase // compile error

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Text("Hello World !")
                }
            }
        }
    }
}

Initially, I attempted to resolve this issue by including the DomainModule in the app layer's module, as shown below. However, I doubt this is an optimal solution:

@InstallIn(SingletonComponent::class)
@Module(
    includes = [
        DomainModule::class
    ]
)
interface DataModule

I'm seeking guidance on whether it is feasible to use Dagger exclusively in the domain layer and Hilt in the remaining layers (app and data). If so, how can I correctly implement this architecture?

Thank you for your assistance!


Solution

  • Firstly, the @Inject annotation in the constructor of MyUseCaseImpl is unnecessary since you're instantiating a new instance in the @Provides method. Secondly, for a truly 'pure' domain layer, it should ideally be free of any dependencies, including Dagger. This can be achieved by relocating all dependency injection, including providers and possible binding methods, to the App layer. By doing that, you can skip the need for @Inject within your domain entirely, which means you can move all this stuff elsewhere (e.g. App).


    From my own experience, I'll kindly add :) that having a domain that's too 'pure' can sometimes be more trouble than it's worth. So, from a learning perspective, it's worth dividing your project in this way. However, in the end, the juice probably isn't worth the squeeze.

    I have similar feelings about separating the interface from useCase because typically, when using interfaces, we want to allow for swapping its implementations. But when it comes to business logic like useCase, that shouldn't really happen.