As Android development evolves, dependency injection has become an indispensable technique for writing testable, maintainable, and scalable code. Hilt, built on top of Dagger, is a popular dependency injection library recommended by Google for Android apps. Migrating ViewModels to Hilt in XML-based projects can seem daunting at first, but with a step-by-step approach, you can streamline the process effectively. This post will guide you through the process of migrating ViewModels to Hilt in XML-based Android projects with practical examples and best practices.
Why Migrate ViewModels to Hilt?
- Simplified Dependency Injection: Hilt automates much of the boilerplate code associated with dependency injection, reducing manual effort.
- Lifecycle Awareness: Hilt is designed to integrate seamlessly with Android lifecycle components, including ViewModels.
- Testability: Properly injected ViewModels can be easily mocked for unit testing.
- Maintainability: Code becomes more modular, improving long-term maintainability.
Prerequisites
Before starting the migration, ensure you have the following:
- An existing Android project that uses XML layouts.
- Dependencies like
androidx.appcompat:appcompat
andandroidx.constraintlayout:constraintlayout
.
Step 1: Add Hilt Dependencies
First, add the necessary Hilt dependencies to your project-level build.gradle
file:
buildscript {
ext {
hilt_version = '2.48' // Use the latest version
}
dependencies {
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
}
}
Then, in your app-level build.gradle
file, add the Hilt and AndroidX dependencies:
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
android {
// Your Android configuration here
}
dependencies {
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
kapt "androidx.hilt:hilt-compiler:1.1.0"
implementation "androidx.hilt:hilt-lifecycle-viewmodel:1.1.0" // Optional: for ViewModel integration
// Other dependencies
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}
Ensure that you apply the Kotlin KAPT plugin and the Hilt plugin in your app-level build.gradle
. Also, sync your project to download the new dependencies.
Step 2: Annotate Your Application Class
Hilt requires an application class annotated with @HiltAndroidApp
. If you don’t have one, create it:
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class MyApplication : Application()
Make sure to update your AndroidManifest.xml
file to use your custom application class:
<application
android:name=".MyApplication"
android:label="@string/app_name">
<!-- Other application attributes -->
</application>
Step 3: Create Your ViewModel
Create your ViewModel class. If it has any dependencies, you can inject them via constructor injection.
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class MyViewModel @Inject constructor(
// Dependencies, such as repositories or use cases
) : ViewModel() {
// ViewModel logic goes here
fun getData(): String {
return "Data from ViewModel"
}
}
Annotate the ViewModel with @HiltViewModel
to enable Hilt’s constructor injection. Use @Inject constructor
to inject any dependencies.
Step 4: Inject ViewModel into Your Activity/Fragment
To inject the ViewModel into your Activity or Fragment, you need to use the ViewModelProvider
and the @AndroidEntryPoint
annotation.
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.activity.viewModels
import dagger.hilt.android.AndroidEntryPoint
import androidx.lifecycle.ViewModelProvider
import android.widget.TextView
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val myViewModel: MyViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val textView: TextView = findViewById(R.id.textView)
textView.text = myViewModel.getData()
}
}
Key points:
- Annotate your Activity with
@AndroidEntryPoint
to enable Hilt injection. - Use
by viewModels()
delegate to obtain the ViewModel instance, injected by Hilt.
Step 5: Providing Dependencies (If Any)
If your ViewModel depends on other components (e.g., repositories, use cases), you must provide these dependencies through Hilt modules.
Create a Hilt module to provide these dependencies:
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.scopes.ViewModelScoped
// Assume this is a simple data source
class MyDataSource {
fun provideData(): String {
return "Data from DataSource"
}
}
@Module
@InstallIn(ViewModelComponent::class)
object MyModule {
@Provides
@ViewModelScoped
fun provideMyDataSource(): MyDataSource {
return MyDataSource()
}
}
In this example:
@Module
indicates that this class provides dependencies.@InstallIn(ViewModelComponent::class)
specifies that the dependencies provided by this module are available in the ViewModel scope.@Provides
annotates methods that provide instances of dependencies.@ViewModelScoped
ensures that the provided instances live as long as the ViewModel.
Update the ViewModel to use the provided dependency:
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class MyViewModel @Inject constructor(
private val myDataSource: MyDataSource
) : ViewModel() {
fun getData(): String {
return myDataSource.provideData()
}
}
Step 6: Test Your Implementation
Write unit tests to ensure that your ViewModel is properly injected and functions as expected.
import org.junit.Test
import org.junit.Assert.assertEquals
import org.mockito.Mockito.mock
import org.mockito.Mockito.`when`
class MyViewModelTest {
@Test
fun testGetData() {
val dataSource = mock(MyDataSource::class.java)
`when`(dataSource.provideData()).thenReturn("Mocked Data")
// Pass the mocked data source when creating ViewModel instance
val viewModel = MyViewModel(dataSource)
assertEquals("Mocked Data", viewModel.getData())
}
}
In this example, Mockito is used to mock the MyDataSource
dependency and verify that the ViewModel behaves correctly under test conditions.
Troubleshooting
- Hilt compilation errors: Ensure all necessary dependencies are added to
build.gradle
, and the Kotlin KAPT plugin and Hilt plugin are correctly configured. - Injection errors: Verify that all dependencies required by the ViewModel are properly provided in Hilt modules and that your Application, Activities, and Fragments are correctly annotated with
@HiltAndroidApp
and@AndroidEntryPoint
respectively. - Runtime crashes: Double-check that the custom Application class is correctly declared in
AndroidManifest.xml
.
Conclusion
Migrating ViewModels to Hilt in XML projects enhances code maintainability, simplifies dependency injection, and streamlines testing. By following the outlined steps, you can seamlessly integrate Hilt into your existing XML-based Android projects and unlock its benefits for managing dependencies in your ViewModels.