A Beginner's Guide to Building Better Android Apps
Understanding DI concepts, benefits, and practical implementation
Press โ to continue
๐กDependency Injection (DI) is a design pattern that provides dependencies to an object instead of the object creating them itself.
Think of it like ordering food at a restaurant:
๐DI makes your Android apps more modular, testable, and maintainable!
class UserRepository { private DatabaseHelper dbHelper; private NetworkService networkService; public UserRepository() { // Creating dependencies inside the class dbHelper = new DatabaseHelper(); networkService = new NetworkService(); } public User getUser(int id) { // Implementation using dbHelper and networkService return networkService.fetchUser(id); } }
class UserRepository { private DatabaseHelper dbHelper; private NetworkService networkService; // Dependencies are injected via constructor public UserRepository(DatabaseHelper dbHelper, NetworkService networkService) { this.dbHelper = dbHelper; this.networkService = networkService; } public User getUser(int id) { return networkService.fetchUser(id); } }
๐กConstructor injection is preferred because it ensures dependencies are available when the object is created.
Framework | Complexity | Performance | Best For |
---|---|---|---|
Dagger 2 | High | Excellent | Large apps |
Hilt | Medium | Excellent | Most Android apps |
Koin | Low | Good | Kotlin projects |
๐ฏHilt is Google's recommended solution for dependency injection in Android.
๐๏ธHilt is built on top of Dagger 2 and provides a simpler way to implement DI in Android apps.
dependencies { implementation "com.google.dagger:hilt-android:2.44" kapt "com.google.dagger:hilt-compiler:2.44" }
First, annotate your Application class with @HiltAndroidApp:
@HiltAndroidApp class MyApplication : Application() { // Application logic here }
And don't forget to register it in AndroidManifest.xml:
๐กThis triggers Hilt's code generation and sets up the DI container.
Create a module to provide dependencies:
@Module @InstallIn(SingletonComponent::class) object NetworkModule { @Provides @Singleton fun provideRetrofit(): Retrofit { return Retrofit.Builder() .baseUrl("https://api.example.com/") .addConverterFactory(GsonConverterFactory.create()) .build() } @Provides fun provideApiService(retrofit: Retrofit): ApiService { return retrofit.create(ApiService::class.java) } }
๐ง@InstallIn
specifies which component should contain the module.
@AndroidEntryPoint class MainActivity : AppCompatActivity() { @Inject lateinit var userRepository: UserRepository @Inject lateinit var apiService: ApiService override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // Use injected dependencies loadUserData() } private fun loadUserData() { // userRepository and apiService are ready to use! } }
โจ@AndroidEntryPoint
enables DI for Android components.
@HiltViewModel class UserViewModel @Inject constructor( private val userRepository: UserRepository ) : ViewModel() { private val _users = MutableLiveData<List<User>>() val users: LiveData<List<User>> = _users fun loadUsers() { viewModelScope.launch { val userList = userRepository.getUsers() _users.value = userList } } }
In your Activity/Fragment:
For classes you own, use @Inject on the constructor:
class UserRepository @Inject constructor( private val apiService: ApiService, private val userDao: UserDao ) { suspend fun getUsers(): List<User> { return try { val remoteUsers = apiService.getUsers() userDao.insertUsers(remoteUsers) remoteUsers } catch (e: Exception) { userDao.getAllUsers() } } suspend fun getUserById(id: Int): User? { return userDao.getUserById(id) } }
โกHilt automatically knows how to create this class and inject its dependencies!
Scopes control the lifetime of dependencies:
Scope | Lifetime | Use Case |
---|---|---|
@Singleton |
App lifetime | Database, Network clients |
@ActivityScoped |
Activity lifetime | Activity-specific services |
@FragmentScoped |
Fragment lifetime | Fragment-specific data |
DI makes testing much easier by allowing mock dependencies:
@HiltAndroidTest class UserRepositoryTest { @get:Rule var hiltRule = HiltAndroidRule(this) @Inject lateinit var userRepository: UserRepository @Before fun setup() { hiltRule.inject() } @Test fun testGetUsers() { // Test your repository with injected dependencies val users = userRepository.getUsers() assertNotNull(users) } }
๐ญYou can easily replace real dependencies with test doubles using @TestInstallIn
.
Here's how all pieces fit together:
// 1. Application Class @HiltAndroidApp class MyApp : Application() // 2. Module @Module @InstallIn(SingletonComponent::class) object AppModule { @Provides @Singleton fun provideUserRepo(api: ApiService) = UserRepository(api) } // 3. Activity @AndroidEntryPoint class MainActivity : AppCompatActivity() { private val viewModel: UserViewModel by viewModels() } // 4. ViewModel @HiltViewModel class UserViewModel @Inject constructor( private val repository: UserRepository ) : ViewModel()
@AndroidEntryPoint
@InstallIn
๐กMost errors are caught at compile-time, making debugging easier!
@Singleton
for expensive objects@Provides @Singleton fun provideExpensiveService(): ExpensiveService { return ExpensiveService() // Created only once }
Start implementing DI in your Android projects! Begin with simple dependencies and gradually adopt more advanced features.
๐ปHappy Coding with Dependency Injection!