When you generate an Android app with AI, one question always pops up: "Do I need Dependency Injection?" The honest answer? It depends. Let me walk you through when DI is essential and when it's just framework bloat.
What is Dependency Injection?
Dependency Injection is a design pattern where objects receive their dependencies from external sources instead of creating them internally. Instead of:
class UserRepository {
private val database = Database()
}
You do:
class UserRepository(private val database: Database)
The caller decides what Database implementation to pass. This makes testing easier (you can pass a mock) and your code more flexible.
Why Should You Care?
Testability: In tests, you inject a fake database instead of hitting a real one.
Flexibility: Swap implementations without changing the class.
Maintainability: Dependencies are explicit and visible.
But here's the catch: these benefits only matter if you're actually testing or swapping implementations. A simple app that directly uses Firebase? You might not need DI at all.
The Three Approaches
1. Manual Injection (No Framework)
You pass dependencies through constructors. Simple, zero magic.
// Service layer
class UserService(private val repository: UserRepository) {
fun getUser(id: Int) = repository.fetchUser(id)
}
// Repository layer
class UserRepository(private val api: UserApi) {
fun fetchUser(id: Int) = api.getUser(id)
}
// In your Activity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val api = UserApi()
val repository = UserRepository(api)
val service = UserService(repository)
// Use service
}
}
Pros:
- Zero dependencies
- Full control
- Easy to understand
- Fast
Cons:
- Verbose setup in large apps
- You must remember the correct order
- No automatic lifecycle management
2. Hilt (Google's Official Solution)
Hilt is a dependency injection library that uses annotations. Google recommends it for production apps.
// Define how to provide the Database
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Singleton
@Provides
fun provideDatabase(@ApplicationContext context: Context): Database {
return Database.build(context)
}
}
// Inject into repository
@Singleton
class UserRepository @Inject constructor(private val database: Database) {
fun fetchUser(id: Int) = database.getUser(id)
}
// Inject into service
@Singleton
class UserService @Inject constructor(private val repository: UserRepository) {
fun getUser(id: Int) = repository.fetchUser(id)
}
// In your Activity (automatic injection)
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var userService: UserService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// userService is automatically injected!
}
}
Pros:
- Google-official
- Handles Android lifecycle automatically
- Compile-time verification
- Scales well to large apps
Cons:
- Learning curve (annotations, modules)
- Dependency: adds ~1MB to APK
- Slower app startup (tiny overhead)
- Overkill for simple apps
3. Koin (Lightweight Alternative)
Koin is a service locator / DI library that's lighter than Hilt.
// Define modules
val appModule = module {
single<Database> { Database.build(androidContext()) }
single<UserRepository> { UserRepository(get()) }
single<UserService> { UserService(get()) }
}
// In Application
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidLogger()
androidContext(this@MyApp)
modules(appModule)
}
}
}
// In Activity
class MainActivity : AppCompatActivity() {
private val userService: UserService by inject()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// userService is available!
}
}
Pros:
- Lightweight (~100KB)
- Easy to understand
- Runtime flexible
Cons:
- Not type-safe at compile time
- No automatic lifecycle management
- Less official support
- Requires runtime reflection
When Should You Actually Use DI?
✅ Use Manual Injection if:
- Your app is small (< 5 screens)
- You're not unit testing
- You want zero overhead
- You're learning Android
Example: Simple todo list app that uses Firebase directly.
✅ Use Hilt if:
- Your app has 10+ screens
- You have multiple implementations to swap (e.g., real API vs mock)
- You're writing unit tests
- You're building for a large team
- You're planning to scale the app
Example: Production app with offline-first sync, multiple data sources, comprehensive tests.
✅ Use Koin if:
- You want DI benefits but lighter than Hilt
- You need runtime flexibility
- You're prototyping quickly
Example: MVP app that might pivot to multiple API backends.
What AI-Generated Apps Should Do
When I generate Android apps, I use manual injection for small templates because:
- Simple logic: Most AI-generated apps don't have complex dependency graphs
- No framework overhead: Users can understand the full flow
- Easier to modify: No magic annotation processing
- Closer to vanilla Kotlin: Teaches clean architecture without obscuring it
However, if you're building a production app with tests and multiple features, you should graduate to Hilt. It's not about being "professional" — it's about managing complexity.
A Practical Example: From Manual to Hilt
Let's say you start with manual injection:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val repo = UserRepository(UserApi())
val viewModel = MyViewModel(repo)
}
}
As your app grows, you add more screens:
class HomeActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val repo = UserRepository(UserApi()) // Duplicated!
val service = UserService(repo)
}
}
class SettingsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val repo = UserRepository(UserApi()) // Duplicated again!
}
}
Now you're duplicating setup code. This is where you introduce Hilt:
@AndroidEntryPoint
class HomeActivity : AppCompatActivity() {
@Inject lateinit var service: UserService
// Done! No boilerplate.
}
@AndroidEntryPoint
class SettingsActivity : AppCompatActivity() {
@Inject lateinit var service: UserService
// Done! And it's the same instance (singleton).
}
Hilt eliminates the duplication.
The Real Answer
Do you need DI? No. You can build a successful Android app without any DI framework.
But DI is useful when:
- You're tired of duplicating setup code
- You want to write unit tests
- You're managing multiple implementations
- Your team is growing
Start simple. Add DI when the pain of manual injection outweighs the learning curve.
My 8 templates use clean manual injection — no framework bloat. See them at https://myougatheax.gumroad.com
Top comments (0)