DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Do AI-Generated Android Apps Need Dependency Injection? The Honest Answer

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()
}
Enter fullscreen mode Exit fullscreen mode

You do:

class UserRepository(private val database: Database)
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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!
    }
}
Enter fullscreen mode Exit fullscreen mode

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!
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Simple logic: Most AI-generated apps don't have complex dependency graphs
  2. No framework overhead: Users can understand the full flow
  3. Easier to modify: No magic annotation processing
  4. 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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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!
    }
}
Enter fullscreen mode Exit fullscreen mode

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).
}
Enter fullscreen mode Exit fullscreen mode

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)