Skip to main content
  1. Blogs/

Dependency Injection is not needed for mobile apps!

·5 mins

Dependency Injection is not needed for mobile apps! #

Last week, I had a heated discussion with our lead architect about dependency injection in our Android app. Their stance was interesting: “DI is unnecessary overhead for mobile development. Just instantiate what you need, where you need it. Besides, we don’t need unit tests - we have QA testers who can catch issues.”

Two arguments I strongly disagreed with. While manual QA testing is valuable, it’s not a replacement for unit tests, but that’s a topic for another post. Today, let me focus on why I believe DI is crucial for Android development.

The perfect example landed in our laps just last month. We had to switch analytics providers, and we spent three days hunting down and replacing hardcoded analytics calls scattered across multiple ViewModels and Repositories. With proper dependency injection, it would have been a 10-minute configuration change. That’s when I decided to write this post.

If you’re an Android developer who’s been told DI is “overengineering,” or you’re simply curious about why it matters, this post is for you.

What’s Dependency Injection, Really? #

Forget the textbook definitions for a minute. Think of dependency injection like ordering a pizza. When you make a pizza at home, you need to prepare the dough, sauce, and toppings yourself (creating your own dependencies). But when you order one, the pizza place handles all that - you just enjoy the final product.

Here’s a real-world Android example. Instead of this:

class UserProfile {
    // Hardcoded dependencies - nightmare to change or test
    private val analytics = FirebaseAnalytics()
    private val userRepository = UserRepositoryImpl(RetrofitInstance.api)
    private val imageLoader = GlideImageLoader()
    
    fun updateProfile() {
        // What if we need to switch analytics providers?
        analytics.logEvent("profile_updated")
        // What if we need to test this with a mock repository?
        viewModelScope.launch {
            userRepository.updateProfile(...)
        }
    }
}

You write this:

class UserProfile @Inject constructor(
    private val analytics: AnalyticsProvider,
    private val userRepository: UserRepository,
    private val imageLoader: ImageLoader
) {
    
    fun updateProfile() {
        analytics.logEvent("profile_updated")
        viewModelScope.launch {
            userRepository.updateProfile(...)
        }
    }
}

Why It Matters for Android Development #

  1. Library Evolution: Remember when we all had to migrate from RxJava to Coroutines? Or when Glide became the preferred image loading solution over Picasso? With DI, swapping out deprecated libraries or switching to better alternatives becomes a configuration change rather than a massive refactoring effort.

  2. Feature Changes: When product decides that user preferences need to be moved from SharedPreferences to DataStore, or that local cache should use Room instead of SQLite, DI lets you make these changes in one place. No need to hunt through dozens of files updating implementations.

  3. Testing: “We don’t need unit tests, we have QA testers!” I hear this far too often in mobile development. While manual testing is crucial, it can’t catch everything, and it’s slow and expensive to rely on it exclusively. With DI, unit testing becomes not just possible, but practical:

@Test
fun `test profile update logs analytics event and calls api`() {
    val mockAnalytics = MockAnalyticsProvider()
    val mockRepository = MockUserRepository()
    val userProfile = UserProfile(
        analytics = mockAnalytics,
        userRepository = mockRepository,
        imageLoader = MockImageLoader()
    )
    
    userProfile.updateProfile()
    
    assertTrue(mockAnalytics.events.contains("profile_updated"))
    assertTrue(mockRepository.updateProfileCalled)
}
  1. Offline Development: Backend team still working on that new API? Inject a mock repository and keep building:
class MockUserRepository : UserRepository {
    override suspend fun updateProfile(data: ProfileData): Result<Profile> {
        return Result.success(Profile(/* mock data */))
    }
}

Setting Up DI with Hilt #

Here’s a practical example using Hilt, Android’s recommended DI solution:

@HiltAndroidApp
class MyApplication : Application()

@Module
@InstallIn(SingletonComponent::class)
object AppModule {
    @Provides
    @Singleton
    fun provideAnalytics(): AnalyticsProvider {
        return when (BuildConfig.BUILD_TYPE) {
            "debug" -> DebugAnalytics()
            else -> FirebaseAnalytics()
        }
    }
    
    @Provides
    @Singleton
    fun provideUserRepository(
        api: ApiService,
        cache: UserCache
    ): UserRepository {
        return UserRepositoryImpl(api, cache)
    }
}

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject lateinit var analytics: AnalyticsProvider
    // Dependencies automatically injected!
}

Why You’ll Love Dependency Injection #

Dependency injection transforms how we test and iterate on Android apps. When you want to test a new image loading library, you simply provide a different ImageLoader implementation based on your experiment group. Hilt takes care of memory management and scoping automatically, helping prevent the memory leaks that often plague Android apps. And during development, you’re no longer blocked waiting for APIs - just inject mocks and keep building.

The Android Specific Angle #

For Android developers, dependency injection isn’t just about clean code. After adopting DI in our app, we saw benefits that manual testing could never match. Our unit tests now run in milliseconds since we can inject mock repositories instead of hitting real APIs. While our QA team excels at catching UI and integration issues, they can’t possibly test every edge case in our business logic - but our unit tests can run through thousands of scenarios in seconds.

The Bottom Line #

Yes, dependency injection requires some upfront setup. Yes, it might seem like “overengineering” for a small app. But Android apps rarely stay small, and the cost of not using DI grows exponentially with your codebase.

Remember that architect I mentioned at the start? The discussion took an interesting turn when I pulled up the official Android documentation. Not only does the Android team recommend dependency injection, but they’ve actually created Hilt specifically for Android development. As the docs state: “Dependency injection is a technique widely used in programming and well suited to Android development.”

The clincher? We were already using their example architecture, Guide to App Architecture, which heavily relies on dependency injection. The same patterns that power official Android samples like Now in Android and Architecture Samples.

Two weeks after our architect agreed to try Hilt in one feature module, we had to switch crash reporting services. That module was the only one we didn’t have to modify. A month later, dependency injection has become our standard practice across all new features.

The best argument for DI wasn’t my explanations or examples - it was Google’s own Android team making it a cornerstone of their recommended architecture. Sometimes the best way to prove a point is to have the platform creators back you up.