Migrating ViewModels to Hilt in XML Projects

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 and androidx.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.