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!