Why Dependency Injection Matters in Android Development
When I started my career in Android development eight years ago, I didn't understand why dependency injection was such a big deal. I'd hardcode database instances, create singletons everywhere, and wonder why my unit tests were brittle and my apps crashed in production.
That changed when I joined CodeBrew Labs and took over a codebase with 6 production apps on the Play Store. One of those apps had a 12% crash rate. The culprit? Tightly coupled dependencies that made testing impossible and created subtle lifecycle bugs that only appeared in real user scenarios.
Today, dependency injection (DI) is non-negotiable for any serious Android project. It's the foundation of clean architecture in Android and makes the difference between code that works and code that scales. In this post, I'll share everything I've learned about implementing DI in production Android apps—both Hilt and Koin—based on real experience with apps serving 50K+ users.
Hilt: Google's Official Approach
Hilt is Google's opinionated dependency injection framework built on top of Dagger 2. When it was released, I was skeptical—Dagger had a steep learning curve and generated confusing compilation errors. But Hilt changed that. It's designed specifically for Android and removes 90% of the boilerplate.
Why I Chose Hilt for EmpSuite ERP
For EmpSuite, our enterprise resource planning platform, I needed a DI solution that could handle complex dependency graphs without sacrificing compile time. Hilt fit perfectly because:
- Built-in Android component integration (Activities, Fragments, Services, BroadcastReceivers)
- Automatic lifecycle management tied to Android components
- Excellent compile-time safety and error messages
- Official Google support and long-term stability
- Works seamlessly with modern Android architecture patterns
Here's a practical example of how I set up Hilt in a production Android app:
// Step 1: Add @HiltAndroidApp to your Application class
@HiltAndroidApp
class MyApplication : Application()
// Step 2: Create a module for database dependencies
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Singleton
@Provides
fun provideAppDatabase(context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"app_database"
).build()
}
@Singleton
@Provides
fun provideUserDao(database: AppDatabase): UserDao {
return database.userDao()
}
}
// Step 3: Create a repository module
@Module
@InstallIn(SingletonComponent::class)
object RepositoryModule {
@Singleton
@Provides
fun provideUserRepository(
userDao: UserDao,
apiService: ApiService
): UserRepository {
return UserRepository(userDao, apiService)
}
}
// Step 4: Inject in your Activity or ViewModel
@HiltViewModel
class UserViewModel @Inject constructor(
private val userRepository: UserRepository
) : ViewModel() {
fun loadUsers() {
// userRepository is automatically injected
}
}This setup ensures that UserRepository is created once, lives for the entire app lifecycle, and is automatically provided whenever needed. No manual instantiation, no memory leaks.
When Hilt Feels Like Overkill
I'll be honest—Hilt has trade-offs. The annotation processing adds compile time. If you're working on a small side project or a simple feature module, the overhead might not be worth it. I've had projects where compile time jumped from 45 seconds to 75 seconds after integrating Hilt across a large codebase.
Koin: The Pragmatic Alternative
Koin is a service locator framework that uses a DSL to define dependencies. It's runtime-based, which means no annotation processing and significantly faster builds. I used Koin extensively in my freelance work on Upwork because clients often had tight deadlines and needed rapid iterations.
Setting Up Koin for Quick Prototyping
For AudioBook AI (which grew to 50K+ users), I started with Koin because I needed to move fast and didn't know the full dependency graph upfront. Here's how I structured it:
// Step 1: Define your modules
val appModule = module {
// Singletons
single { AppDatabase.getDatabase(androidContext()) }
single { get<AppDatabase>().userDao() }
// Repositories
single { UserRepository(get()) }
single { BookRepository(get()) }
// ViewModels
viewModel { UserViewModel(get()) }
viewModel { BookViewModel(get()) }
}
val networkModule = module {
single { OkHttpClient.Builder().build() }
single {
Retrofit.Builder()
.baseUrl("https://api.example.com")
.client(get())
.addConverterFactory(GsonConverterFactory.create())
.build()
}
single { get<Retrofit>().create(ApiService::class.java) }
}
// Step 2: Start Koin in your Application class
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@MyApplication)
modules(appModule, networkModule)
}
}
}
// Step 3: Inject in your Activities/Fragments
class UserActivity : AppCompatActivity() {
private val userViewModel: UserViewModel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// userViewModel is ready to use
}
}The beauty of Koin is simplicity and flexibility. You can reorganize your dependency graph without recompiling. For iterative development, this is invaluable.
The Hidden Cost of Runtime Resolution
Here's where I'd caution you: Koin resolves dependencies at runtime. If you have a missing dependency, you'll discover it when that code path executes, not at compile time. In large teams, this can lead to subtle bugs slipping into production.
⚠️ Watch Out
I once had a Koin dependency misconfiguration that only surfaced when a specific user flow triggered it in production. No unit test caught it because the test never instantiated that class. Hilt would have caught this at compile time.
Real-World Comparison from Production Apps
After leading a 4-engineer squad at Raybit Technologies, I've seen both frameworks in large production codebases. Here's my unfiltered comparison:
Hilt Advantages
- Compile-time safety: Missing dependencies cause build failures, not runtime crashes
- Built-in Android integration: Automatic lifecycle binding for Activities, Fragments, Services
- Scoping: Easy to define component-scoped dependencies (Activity-scoped, Fragment-scoped)
- Team safety: Harder for junior developers to make mistakes
- Long-term support: Google maintains it as part of Jetpack
Koin Advantages
- Build speed: No annotation processing, significantly faster compilation
- Learning curve: DSL is easier to understand than Dagger/Hilt annotations
- Flexibility: Runtime resolution allows dynamic dependency swapping
- Small projects: Perfect for MVPs and prototype apps
- Kotlin-first: Designed with Kotlin idioms in mind
"After 8 years of Android development, my rule is simple: use Hilt for production apps with multiple engineers. Use Koin for solo projects, MVPs, and rapid prototyping."
Migration Strategies for Existing Projects
If you're using legacy dependency injection (or no DI at all), migrating to a modern solution is challenging but worth it. I led a migration on one of CodeBrew's apps that reduced crash rate by 35% just by improving dependency lifecycle management.
Incremental Migration to Hilt
Don't rip and replace. Do this:
- Start with one feature module: Add Hilt to a single, isolated feature first
- Migrate data layer first: Move database and API clients to Hilt modules
- Then repositories: Inject repositories into ViewModels
- Finally, Activities/Fragments: Last step is wiring up the UI layer
This approach lets you test each layer independently and catch issues before they affect the entire app.
Keeping Koin for Specific Modules
You can also run both frameworks in the same app. One of our Raybit projects used Hilt for the core app and Koin for a legacy module that wasn't worth rewriting. It worked, but adds complexity—I only recommend this if you have no choice.
📖 Pro Tip
When migrating, write integration tests for your dependency graph. I use a simple test that verifies all major components can be instantiated. It catches 80% of DI issues before they reach QA.
Key Takeaways
- Dependency Injection is foundational to Android architecture. It enables testing, scales with team size, and prevents lifecycle-related crashes. I wouldn't ship a production app without it.
- Choose Hilt for production teams, Koin for solo/rapid development. Hilt's compile-time safety wins in large codebases. Koin's simplicity and build speed win for quick iterations and MVPs.
- Migration is incremental, not overnight. Start with the data layer, move up the stack. Your existing code doesn't need to be perfect—DI frameworks integrate with legacy code.
- Invest in test infrastructure. Good DI means better testability. The real win isn't just cleaner code—it's unit tests that actually catch bugs before production.
- Both frameworks are mature and production-ready. Pick one and learn it deeply rather than switching between them. Mastery matters more than the choice.