Benchmarking Koin vs. Dagger Hilt in Modern Android Development (2024)
Benchmarking Koin vs. Dagger Hilt in Modern Android Development (2024) ź“ė Ø
When choosing a dependency injection framework for Android and Kotlin development, performance is often a key consideration. This article explores the performance ofĀ KoinĀ in its latest version (4.0.1-Beta1) and compares it withĀ Dagger Hilt (2.52). Rather than relying on simplistic benchmarks or limited code execution scenarios, the focus is ādeveloper-centricā: understanding performance in real-world, day-to-day usage. Additionally, this article aims to reassure those who may hesitate to adoptĀ KoinĀ due to performance concerns.
What to benchmark?
Benchmarking such frameworks poses a significant challenge: ensuring fair comparison and focused on equivalent behaviors and features.
To make this exercise meaningful, Iāve opted for a user-oriented approach: evaluating the time it takes to build a component requested from the UI (like ViewModels and so on ā¦). To ensure our test context is strong enough, we need a complex enough application (no basic āHello Worldā or to-do list app).
For this purpose, Iāve chosen to use GoogleāsĀ Now in Android app (android/nowinandroid
), a great open-source application that is complex enough and covers the challenges of real-life development and where the Android team demonstrates best practices (modularization, Jetpack Compose, and dependency injection ā¦).
By evaluating Koin and Dagger Hilt in this environment, we aim to get insights that truly matter to Android developers.
Note
š Sources are available atĀ InsertKoinIO/nowinandroid
You will find the following branches:
perfs_koin
Ā ā is the Now in Android migrated to Koin branch, with performances measurementperfs_hilt
ā is the default Hilt branch, with performances measurement
And donāt forgetĀ official Koin documentation. Now, letās dive into the details!
Service Locator or Dependency Injection? Koin can do both!
Before diving into the benchmarks, letās address a common question about Koin: Is it a Service Locator or a Dependency Injection (DI) framework? The answer is both.
- AĀ Service LocatorĀ retrieves dependencies dynamically through a centralized registry.
- Dependency InjectionĀ provides dependencies explicitly at instantiation, enhancing testability and maintainability.
Koin bridges these two approaches, offering dynamic retrieval viaĀ get()
Ā orĀ inject()
Ā while also supporting DI features like constructor injection and scoping.
Koinās dynamic behavior is influenced by Androidās lifecycle, which historically made constructor injection challenging. While modern Android features now support constructor injection, Koin remains flexible, letting developers choose the best approach for their needs.
At its core,Ā Koin is a DI framework. It avoids reflection overhead, uses a Kotlin DSL for dependency graphs, and supports scoped lifecycles. However, its ability to function as a Service Locator adds versatility, particularly for simpler or legacy projects.
This is a summary, but this Koin projectĀ documentation pageĀ has more details if you need to go deeper.
Why choose Koin?
- Simple and Developer-Friendly: Koinās clean DSL, no compile-time overhead, minimal setup, and easy testing let you focus on building your apps.
- Scales with Your App: from small apps to complex projects, Koin scales effortlessly to meet your needs.
- Evolving Compile-Time Safety: With features like module validation (Verify API),Ā Koin AnnotationsĀ (KSP for configuration safety), and the upcomingĀ Koin IDE Plugin, Koin simplifies coding while boosting safety.
- Ready for Kotlin Multiplatform: Koin seamlessly manages dependencies across iOS, Android, Desktop, and Web, making it the go-to DI framework for cross-platform development.
- Perfect for Compose Multiplatform: Koin integrates effortlessly with Compose Multiplatform, supporting shared logic and DI for UI components ā evenĀ ViewModel.
If youāre curious about Koinās internals and design, let me know ā Iād be happy to explore that in a future article. For now, letās dive into the benchmarks! š
Tracking Performances
Tracking the performance of components over sessions is trickier than it initially seems. While tools likeĀ Baseline Profiles MacrobenchmarkĀ and similar deep-dive tools offer great analysis, they donāt allow me to easily extract benchmark values for custom use. Alternatively, connected dev platforms likeĀ Firebase Crashlytics or Kotzilla PlatformĀ offer convenient solutions to capture and analyze performance metrics.
My goal here is toĀ stay simple and lightweight: I want toĀ measure how long it takes to create a specific component, like building a ViewModel instance using dependency injection. I donāt need a complex framework for this task, but Iām OK with manually instrumenting my code as long as itās straightforward and lightweight.
To achieve this, I wrote a few functions to capture function call time from DI frameworks (All is inĀ Measure.kt
(InsertKoinIO/nowinandroid
)Ā file). This utility leveragesĀ Kotlinās measureTimedValue function, an elegant and efficient way to measure code execution times, making it an excellent fit for lightweight, manual instrumentation. By extending the AndroidĀ Context
, I created an easy way to log the duration of any function call (or dependency injection operation) directly to a log file.
inline fun <reified T> Context.measureTimeLazy(tag : String, code : () -> Lazy<T>) : Lazy<T>{
val result = measureTimedValue(code)
val timeInMs = result.duration.inWholeMicroseconds / 1000.0
logBenchmark(tag,timeInMs)
return result.value
}
In the end,Ā we are storing all results in a local fileĀ (functionĀ logBenchmark). This file will be extracted, to allow average times calculation.
Now in Android
Now, letās see how these tracking functions are applied in our real-world scenario. For this benchmark, weāll measure the performance of the following components:Ā MainActivityViewModel,Ā ForYouViewModel, andĀ startup time.
These ViewModels are the first two used in the application, making them ideal candidates for assessing the performance of DI frameworks during the appās initial loading phase.
In theĀ Koin implementation, the performance tracking for these components is instrumented as follows (MainActivity.kt
(InsertKoinIO/nowinandroid
)Ā &Ā ForYouScreen.kt
(InsertKoinIO/nowinandroid
) links):
class MainActivity : ComponentActivity() {
/**
* Lazily inject [JankStats], which is used to track jank throughout the app.
*/
val lazyStats by inject<JankStats> { parametersOf(this) }
val networkMonitor: NetworkMonitor by inject()
val timeZoneMonitor: TimeZoneMonitor by inject()
val analyticsHelper: AnalyticsHelper by inject()
val userNewsResourceRepository: UserNewsResourceRepository by inject()
private val viewModel: MainActivityViewModel by measureTimeLazy("MainActivityViewModel") { viewModel() }
// ...
}
@Composable
internal fun ForYouScreen(
onTopicClick: (String) -> Unit,
modifier: Modifier = Modifier,
viewModel: ForYouViewModel = LocalContext.current.measureTime("ForYouViewModel") { koinViewModel() },
) {
val onboardingUiState by viewModel.onboardingUiState.collectAsStateWithLifecycle()
val feedState by viewModel.feedState.collectAsStateWithLifecycle()
val isSyncing by viewModel.isSyncing.collectAsStateWithLifecycle()
val deepLinkedUserNewsResource by viewModel.deepLinkedNewsResource.collectAsStateWithLifecycle()
ForYouScreen(
isSyncing = isSyncing,
onboardingUiState = onboardingUiState,
feedState = feedState,
deepLinkedUserNewsResource = deepLinkedUserNewsResource,
onTopicCheckedChanged = viewModel::updateTopicSelection,
onDeepLinkOpened = viewModel::onDeepLinkOpened,
onTopicClick = onTopicClick,
saveFollowedTopics = viewModel::dismissOnboarding,
onNewsResourcesCheckedChanged = viewModel::updateNewsResourceSaved,
onNewsResourceViewed = { viewModel.setNewsResourceViewed(it, true) },
modifier = modifier,
)
}
Note
We are using the latestĀ Koin AndroidX StartupĀ feature to help improve startup time.
For theĀ Hilt implementation, tracking is similarly applied (MainActivity.kt
(InsertKoinIO/nowinandroid
)Ā &Ā ForYouScreen
(InsertKoinIO/nowinandroid
)Ā links):
To capture theĀ startup time, we use theĀ onWindowFocusChanged
Ā function inĀ MainActivity. This measures the time it takes for the app to render its first frame after gaining focus, giving a clear picture of the appās startup performance. We track time from theĀ ApplicationĀ class until the first Activity:
class MainActivity : ComponentActivity() {
// ...
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
val endTime = System.currentTimeMillis()
val startupTime = endTime - NiaApplication.startTime
logBenchmark("AppStartup",startupTime.toDouble())
}
}
// ...
}
Execution, Extraction, And Results
To capture performance metrics automatically, we run theĀ benchmark.sh
Ā shell script. This script automates a sequence of app install, start, wait a few seconds, and stop actions to simulate realistic usage patterns. After all runs, it extracts theĀ benchmark_log.txt
Ā file containing all recorded times. This is 25 iterations of running the Nia applicationās start, wait and stop (demo release build).
Using the collected data, theĀ stats.py
Ā Python script processes the log to compute key statistics: minimum, maximum, and average times for each benchmarked component.
On your terminal, you can just run the command:Ā benchmark.sh; python3 stats.py
(from the /app
folder).
The best is to run it on a real Android device. On my OnePlus Nord (Android 12), I got the following results:
OnePlus NordĀ results (arnaudgiuliani
), and also in GoogleĀ spreadsheet
Benchmark Results
Same OnePlus NordĀ results (arnaudgiuliani
) in table
Component | Framework | Avg (ms) | Min (ms) | Max (ms) | Standard Error ( ms) |
---|---|---|---|---|---|
MainActivityViewModel | Koin | 0.166 | 0.146 | 0.198 | 0.002 |
MainActivityViewModel | Hilt | 0.202 | 0.186 | 0.264 | 0.003 |
ForYouViewModel | Koin | 2.052 | 0.223 | 9.042 | 0.302 |
ForYouViewModel | Hilt | 2.203 | 0.359 | 8.481 | 0.299 |
App Startup | Koin | 1416.360 | 1204.000 | 1746.000 | 37.072 |
App Startup | Hilt | 1511.480 | 1238.000 | 1729.000 | 35.457 |
In this benchmark, in addition to average, minimum, and maximum, we show the āstandard errorā: it measures theĀ reliability of the average, indicating how much it may vary from the true population mean. Smaller values mean more stable and precise results. It helps also compare stability results between Koin and Dagger Hilt.
The benchmarks highlight KoinĀ as a reliable and modern alternative for Android development, matchingĀ HiltĀ in performance while offering its own unique advantages.
That said, benchmarks are just one part of the story. Your results may vary depending on your app, but the trends are clear:Ā Koin is performant for real-world challenges. From Android to Kotlin Multiplatform and Compose Multiplatform applications.
Iām always open to feedback ā if you have thoughts or insights,Ā letās chat! š
Why not giveĀ KoinĀ a shot? LetĀ KoinĀ be part of your journey! š
Info
This article is previously published on proandroiddev