Dependency Injection with Koin
Relevant source files
The following files were used as context for generating this wiki page:
The Vocabri project utilizes Koin for dependency injection to manage the instantiation and provision of classes throughout the application. This approach promotes a clean, decoupled architecture by separating concerns into distinct, modular layers. The DI setup is organized across the app, data, and domain modules, each with its own Koin module file.
Koin is initialized at the application's entry point, loading all necessary modules to build the dependency graph. This graph provides everything from database drivers and HTTP clients to repositories, use cases, and ViewModels, ensuring that components receive the dependencies they need without having to construct them manually.
Initialization
Dependency injection is set up when the application starts, within the onCreate method of the VocabriApplication class. The startKoin function is called to initialize the Koin container, providing it with the Android application context and a list of all modules to be loaded.
The initialization sequence is as follows:
- The
VocabriApplication'sonCreatemethod is called. startKoinis invoked.- An Android logger (
androidLogger) is configured for Koin's internal logging. - The application context is provided to Koin via
androidContext. - The
dataModule,domainModule, andappModuleare loaded, making their declared dependencies available for injection.
Sources: app/src/main/java/com/vocabri/VocabriApplication.kt:33-42
// app/src/main/java/com/vocabri/VocabriApplication.kt:36-42
startKoin {
log.i { "Initializing Koin with application context and modules" }
androidLogger()
androidContext(this@VocabriApplication)
modules(
dataModule,
domainModule,
appModule,
)
}
The following diagram illustrates the Koin initialization flow.
Modular Structure
The dependency graph is split across three main modules, corresponding to the project's architectural layers: data, domain, and app. This modularity helps organize dependencies and maintain a clear separation of concerns.
Sources: README.md:32-34, app/src/main/java/com/vocabri/VocabriApplication.kt:40-42
This diagram shows the dependency flow between the Koin modules, which mirrors the architectural layers of the application.
Data Module (dataModule)
The dataModule is responsible for providing all data-related dependencies. This includes the database instance, data sources, repository implementations, and utility classes like the ID generator.
Key dependencies provided by dataModule:
| Dependency | Scope | Implementation | Description |
|---|---|---|---|
VocabriDatabase | single | VocabriDatabase | SQLDelight database instance using AndroidSqliteDriver. |
HttpClient | single | VocabriHttpClientFactory | Ktor HTTP client for network operations. |
WordDataSource | single | WordLocalDataSourceImpl | Local data source for words, interacting with the database. |
RemoteWordDataSource | single | WordRemoteDataSourceStub | Stub implementation for a remote word data source. |
DebugDataSource | single | DebugDataSourceImpl | Data source for debug-related functionalities. |
WordRepository | single | WordRepositoryImpl | Implements the WordRepository interface from the domain layer. |
DebugRepository | single | DebugRepositoryImpl | Implements the DebugRepository interface from the domain layer. |
IdGenerator | single | UuidIdGenerator | Generates unique IDs. |
Sources: data/src/main/java/com/vocabri/data/di/DataModule.kt:35-56
The following diagram shows the relationships between the main components defined in dataModule.
Domain Module (domainModule)
The domainModule provides the application's use cases (interactors). These classes contain the core business logic and orchestrate data flow between the UI and data layers. All use cases are defined as factory dependencies, meaning a new instance is created each time one is requested.
Example use cases provided:
AddWordUseCaseDeleteWordUseCaseGenerateRandomWordUseCaseObserveWordGroupsUseCaseObserveWordsUseCase
These are defined using the factoryOf syntax.
// domain/src/main/java/com/vocabri/domain/di/DomainModule.kt:31-37
val domainModule = module {
factoryOf(::AddWordUseCase)
factoryOf(::ObserveWordsUseCase)
factoryOf(::ObserveWordGroupsUseCase)
factoryOf(::DeleteWordUseCase)
factoryOf(::GenerateRandomWordUseCase)
}
Sources: domain/src/main/java/com/vocabri/domain/di/DomainModule.kt:30-38
App Module (appModule)
The appModule is responsible for UI-layer dependencies, primarily ViewModels. It also provides Android-specific implementations for domain-layer interfaces, such as ResourcesRepository.
Key dependencies provided by appModule:
- ViewModels:
MainViewModel,DictionaryViewModel, andAddWordViewModelare provided usingviewModelOf, which ties their lifecycle to UI components. - Dynamic ViewModels: A
viewModelfactory is dynamically created for eachPartOfSpeechenum entry. This allowsDictionaryDetailsViewModelto be instantiated with a specificPartOfSpeechqualifier, enabling the UI to request a ViewModel tailored to a specific word category (e.g., Nouns, Verbs). - CoroutineScope: A factory for an IO-bound
CoroutineScopeis provided for background tasks. ResourcesRepository: Asingleinstance ofAndroidResourcesRepositoryis bound to theResourcesRepositoryinterface, providing access to Android string resources.
// app/src/main/java/com/vocabri/di/AppModule.kt:47-56
PartOfSpeech.entries.forEach { partOfSpeech ->
viewModel(qualifier = DictionaryDetailsQualifiers.fromPartOfSpeech(partOfSpeech)) {
DictionaryDetailsViewModel(
partOfSpeech = partOfSpeech,
observeWordsUseCase = get(),
deleteWordUseCase = get(),
ioScope = get(),
)
}
}
This snippet demonstrates the dynamic creation of DictionaryDetailsViewModel instances, each qualified by a specific part of speech.
Sources: app/src/main/java/com/vocabri/di/AppModule.kt:41-60
Testing with Koin
For testing purposes, especially in instrumented tests, the project uses a custom JUnit TestRule named KoinTestRule. This rule manages the Koin lifecycle during tests. Before each test, it calls stopKoin() to clear any existing context and then startKoin() with a list of modules specific to the test, which often includes mock or fake implementations. After the test completes, it calls stopKoin() again to ensure a clean state for the next test.
// app/src/androidTest/java/com/vocabri/test/rule/KoinTestRule.kt:29-38
class KoinTestRule(private val modules: List<org.koin.core.module.Module>) : TestRule {
override fun apply(base: Statement, description: Description): Statement = object : Statement() {
override fun evaluate() {
try {
stopKoin()
startKoin { modules(modules) }
base.evaluate()
} finally {
stopKoin()
}
}
}
}
Sources: app/src/androidTest/java/com/vocabri/test/rule/KoinTestRule.kt:29-38
Summary
Vocabri's dependency injection strategy, powered by Koin, establishes a robust and maintainable architecture. By organizing dependencies into data, domain, and app modules, the project achieves a clear separation of concerns. The initialization process in VocabriApplication ensures that all necessary components are available from the start. This modular setup not only simplifies development and improves code readability but also facilitates testing by allowing for the easy replacement of real dependencies with test doubles.