Dependency Injection (DI) is a crucial pattern in modern Android development, fostering testability, maintainability, and scalability. Hilt, built on top of Dagger, simplifies DI in Android apps. This post explores how to migrate Jetpack ViewModels to Hilt, ensuring clean and efficient dependency management.
Why Migrate ViewModels to Hilt?
- Simplified Dependency Management: Hilt automates much of the boilerplate involved in DI.
- Lifecycle Awareness: Hilt is designed to integrate seamlessly with Android’s lifecycle.
- Improved Testability: Makes it easier to provide mock dependencies for testing.
- Reduced Boilerplate: Hilt eliminates the need for manual factory implementations.
Prerequisites
Before starting, ensure you have the following dependencies in your build.gradle file:
dependencies {
implementation("com.google.dagger:hilt-android:2.48")
kapt("com.google.dagger:hilt-compiler:2.48")
implementation("androidx.hilt:hilt-navigation-compose:1.1.0") // For Compose Navigation
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
}
// Add this to your root build.gradle.kts or build.gradle
plugins {
id("com.google.dagger.hilt.android") version "2.48" apply false
}
Step-by-Step Migration Guide
Step 1: Enable Hilt in Your Application
Annotate your Application class with @HiltAndroidApp. This triggers Hilt’s code generation and sets up the dependency injection container.
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class MyApplication : Application()
Don’t forget to declare the Application in your AndroidManifest.xml file:
<application
android:name=".MyApplication"
...>
</application>
Step 2: Annotate Your ViewModel with @HiltViewModel
Replace any manual dependency injection code in your ViewModel with @HiltViewModel. Use @Inject to specify dependencies in the constructor.
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class MyViewModel @Inject constructor(
private val myRepository: MyRepository
) : ViewModel() {
// ViewModel logic here
fun getData() = myRepository.getData()
}
Step 3: Provide Dependencies
Create Hilt modules to provide the necessary dependencies. Use @Module and @Provides to define how dependencies are created.
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideMyRepository(): MyRepository {
return MyRepositoryImpl() // Or however you create your repository
}
}
In this example:
@Moduleindicates that this is a Hilt module.@InstallIn(SingletonComponent::class)specifies that the provided dependencies should live as long as the application.@Providesannotates functions that provide instances of dependencies.@Singletonensures only one instance ofMyRepositoryis created.
Step 4: Use ViewModel in Activities/Fragments/Composables
With Hilt, retrieving the ViewModel is straightforward. In Activities/Fragments:
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
class MainActivity : AppCompatActivity() {
private val viewModel: MyViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Use viewModel
val data = viewModel.getData()
}
}
In Jetpack Compose:
import androidx.compose.runtime.Composable
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.compose.material.Text
@Composable
fun MyComposable(viewModel: MyViewModel = hiltViewModel()) {
val data = viewModel.getData()
Text(text = "Data: $data")
}
Example: A Complete Scenario
Let’s consider a scenario with a Repository and a ViewModel using it.
First, the Repository Interface and Implementation:
interface MyRepository {
fun getData(): String
}
class MyRepositoryImpl : MyRepository {
override fun getData(): String {
return "Data from Repository"
}
}
Then, the Hilt-annotated ViewModel:
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class MyViewModel @Inject constructor(
private val myRepository: MyRepository
) : ViewModel() {
fun getData(): String {
return myRepository.getData()
}
}
And the Hilt Module:
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideMyRepository(): MyRepository {
return MyRepositoryImpl()
}
}
Finally, usage in a Compose Composable:
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.compose.material.Text
@Composable
fun MyScreen(viewModel: MyViewModel = hiltViewModel()) {
val data = viewModel.getData()
Text("Data from ViewModel: $data")
}
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
MyScreen()
}
Testing Hilt ViewModels
Hilt simplifies testing ViewModels. You can create a test module to provide mock dependencies.
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.testing.TestInstallIn
@Module
@TestInstallIn(
components = [ViewModelComponent::class],
replaces = [AppModule::class]
)
object TestAppModule {
@Provides
fun provideTestRepository(): MyRepository {
return FakeMyRepository()
}
}
class FakeMyRepository : MyRepository {
override fun getData(): String {
return "Fake Data"
}
}
In your test:
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import javax.inject.Inject
@HiltAndroidTest
class MyViewModelTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)
@Inject
lateinit var myRepository: MyRepository
@Before
fun setup() {
hiltRule.inject()
}
@Test
fun testGetData() {
val viewModel = MyViewModel(myRepository)
val data = viewModel.getData()
assert(data == "Fake Data")
}
}
Best Practices and Common Issues
- Use Qualifiers When Necessary: If you have multiple implementations of an interface, use
@Qualifierannotations to differentiate them. - Scope Appropriately: Choose the right scope (e.g.,
@Singleton,@ActivityScoped,@ViewModelScoped) based on the lifecycle of the dependency. - Clean Build After Adding Hilt: Always perform a clean build (
./gradlew clean) after adding or modifying Hilt-related code to avoid compilation issues. - Ensure All Dependencies are Provided: Hilt will throw an exception if any required dependency is not provided.
Conclusion
Migrating Jetpack ViewModels to Hilt streamlines dependency management and improves code maintainability and testability. By following these steps, you can leverage Hilt to build cleaner and more robust Android applications. Embrace Hilt for your ViewModels to enjoy the benefits of automated dependency injection and lifecycle awareness in modern Android development.