Introduction
When working with Hilt, you might have wondered how to inject ViewModels into your application. In this tutorial, we will learn how to inject ViewModels into your app Fragments.
Goals
At the end of the tutorial, you would have learned:
- How to inject ViewModels into Fragments.
- Understand injected ViewModel’s lifecycle.
Tools Required
- Android Studio. The version used in this tutorial is Bumblebee 2021.1.1 Patch 1.
Prerequisite Knowledge
- Basic Android.
- Basic Hilt.
Project Setup
To follow along with the tutorial, perform the steps below:
-
Create a new Android project with the default Empty Activity.
-
Create 3 new Fragments called BlankFragment1, BlankFragment2, and BlankFragment3 by right-clicking the main package > New > Fragment > Fragment (Blank).
-
Replace the code inside activity_main.xml with the code below. This removes the default “Hello World!” TextView, adds three Buttons aligned on a vertical chain, and a FragmentContainerView.
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <Button android:id="@+id/button_toFragment1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/to_fragment_1" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <Button android:id="@+id/button_toFragment2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/to_fragment_2" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/button_toFragment1" /> <Button android:id="@+id/button_toFragment3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/to_fragment_3" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/button_toFragment2" /> <androidx.fragment.app.FragmentContainerView android:id="@+id/fragmentContainerView" android:name="com.codelab.daniwebhiltviewmodels.BlankFragment1" android:layout_width="match_parent" android:layout_height="0dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/button_toFragment3" /> </androidx.constraintlayout.widget.ConstraintLayout>
-
Add the code below into MainActivity#
onCreate()
. It binds the three buttons to an action that will always create the respective Fragment, without any backstack.val toFragment1Button = findViewById<Button>(R.id.button_toFragment1) val toFragment2Button = findViewById<Button>(R.id.button_toFragment2) val toFragment3Button = findViewById<Button>(R.id.button_toFragment3) toFragment1Button.setOnClickListener{ supportFragmentManager.commit { replace<BlankFragment1>(R.id.fragmentContainerView) } } toFragment2Button.setOnClickListener { supportFragmentManager.commit { replace<BlankFragment2>(R.id.fragmentContainerView) } } toFragment3Button.setOnClickListener { supportFragmentManager.commit { replace<BlankFragment3>(R.id.fragmentContainerView) } }
-
Annotate MainActivity and all three Fragment classes with
@AndroidEntryPoint
. -
Add the
<string>
resources below into yourstrings.xml
.<string name="to_fragment_1">To Fragment 1</string> <string name="to_fragment_2">To Fragment 2</string> <string name="to_fragment_3">To Fragment 3</string> <string name="hello_blank_fragment">Hello blank fragment</string> <string name="hello_blank_fragment2">Hello blank fragment2</string> <string name="hello_blank_fragment3">Hello blank fragment3</string>
-
Go to each Fragment’s associated layout XML and modify the default TextView’s
android:text
to the respective<string>
resource above. -
Properly tag each Fragment with
BLANK_FRAGMENT_X
, respectively. -
Add the dependency below into your Project build.gradle.
buildscript { dependencies { classpath 'com.google.dagger:hilt-android-gradle-plugin:2.40.5' } }
-
Add the plugins for kapt and hilt to your Module build.gradle.
implementation "com.google.dagger:hilt-android:2.40.5" implementation "androidx.fragment:fragment-ktx:1.4.1" kapt "com.google.dagger:hilt-compiler:2.40.5"
-
Also in the Module build.gradle, add the dependencies below.
implementation "com.google.dagger:hilt-android:2.40.5" kapt "com.google.dagger:hilt-compiler:2.40.5"
-
Create the required Application class for Hilt called MyApplication.kt.
import android.app.Application import dagger.hilt.android.HiltAndroidApp @HiltAndroidApp class ExampleApplication : Application()
-
Add the attribute below to the manifest
<application>
element.android:name=".ExampleApplication"
Project Overview
Our project so far includes three buttons, which will navigate to three different fragments. At the end of the tutorial, we should have created one ViewModel, which will be injected into the existing fragments. The lifecycle of the ViewModel will behave differently depending how it was injected, so we will observe the output from Logcat to understand what happens to the injected ViewModel instances.
Creating the ViewModel
First, let us create the ViewModel class FragmentViewModel. Copy and paste the code below into a new file named FragmentViewModel.kt.
private const val TAG = "FRAGMENT_VIEW_MODEL"
@HiltViewModel
class FragmentViewModel @Inject constructor(private val application: Application) : ViewModel() {
override fun onCleared() {
super.onCleared()
Log.d(TAG, "ViewModel ${hashCode()} is queued to be destroyed.")
}
}
In the FragmentViewModel class above, the annotation @HiltViewModel
tells Hilt that this ViewModel can be injected into other classes marked with @AndroidEntryPoint
as well as allowing Hilt to inject other dependencies into this ViewModel. In this case, we have injected an Application class, which is a special pre-defined binding, only to show that Hilt will assist us in providing instances of this class by injecting this application
dependency.
We have also overridden the onCleared()
function. This function is called when the Android system destroys the ViewModel.
Hilt ViewModels scoped to Fragment lifecycle
New Hilt ViewModels can be created and injected into each Fragment if requested. This means that each Fragment instance will receive different instances of ViewModel.
In BlankFragment1 and BlankFragment2, perform the steps below:
-
Add the
fragmentViewModel
member below. TheviewModels()
property delegate will provide a ViewModel scoped to the Fragment lifecycle.viewModels()
is not only used with Hilt, but it can be used to provide simple ViewModel instances as well when not using Hilt.private val fragmentViewModel: FragmentViewModel by viewModels()
-
Append to
onCreate()
the code below. This will log when the Fragment is created and which ViewModel instance it receives.Log.d(TAG, "Fragment ${hashCode()} is created with ViewModel ${fragmentViewModel.hashCode()} injected.")
-
Override
onDestroy()
and append the code below. Leave thesuper()
call as-is. This will log when the Fragment is destroyed. Shortly after the Fragment is destroyed, we should be able to see that the ViewModel calls itsonClear()
function as well.Log.d(TAG, "Fragment ${hashCode()} is being destroyed.")
While running the App with the Logcat filter BLANK_FRAGMENT|FRAGMENT_VIEW_MODEL
in the Debug channel, we can see that the Fragment/ViewModel pair are being created and destroyed together (when you create another Fragment).
Hilt ViewModels scoped to Activity lifecycle
Hilt can also inject the same ViewModel instance to different instances of the same Fragment. We will apply this behavior to BlankFragment3.
- Repeat the steps that you did in the previous section to BlankFragment3.
- Replace the
viewModel()
delegate withactivityViewModel()
. This will ensure that the ViewModel received is scoped to the Activity’s lifecycle; this can mean the entire application lifecycle in many cases.
When running the app and recreating BlankFragment3 multiple times, we can see that the same ViewModel instance is injected into multiple different instances of BlankFragment3.
Solution Code
MainActivity.kt
package com.codelab.daniwebhiltviewmodels
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import androidx.fragment.app.commit
import androidx.fragment.app.replace
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val toFragment1Button = findViewById<Button>(R.id.button_toFragment1)
val toFragment2Button = findViewById<Button>(R.id.button_toFragment2)
val toFragment3Button = findViewById<Button>(R.id.button_toFragment3)
toFragment1Button.setOnClickListener{
supportFragmentManager.commit {
replace<BlankFragment1>(R.id.fragmentContainerView)
}
}
toFragment2Button.setOnClickListener {
supportFragmentManager.commit {
replace<BlankFragment2>(R.id.fragmentContainerView)
}
}
toFragment3Button.setOnClickListener {
supportFragmentManager.commit {
replace<BlankFragment3>(R.id.fragmentContainerView)
}
}
}
}
FragmentViewModel.kt
package com.codelab.daniwebhiltviewmodels
import android.app.Application
import android.util.Log
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
private const val TAG = "FRAGMENT_VIEW_MODEL"
@HiltViewModel
class FragmentViewModel @Inject constructor(private val application: Application) : ViewModel() {
override fun onCleared() {
super.onCleared()
Log.d(TAG, "ViewModel ${hashCode()} is queued to be destroyed.")
}
}
BlankFragment1.kt
package com.codelab.daniwebhiltviewmodels
import android.os.Bundle
import android.util.Log
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.viewModels
import dagger.hilt.android.AndroidEntryPoint
// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"
private const val TAG = "BLANK_FRAGMENT_1"
/**
* A simple [Fragment] subclass.
* Use the [BlankFragment1.newInstance] factory method to
* create an instance of this fragment.
*/
@AndroidEntryPoint
class BlankFragment1 : Fragment() {
// TODO: Rename and change types of parameters
private var param1: String? = null
private var param2: String? = null
private val fragmentViewModel: FragmentViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
param1 = it.getString(ARG_PARAM1)
param2 = it.getString(ARG_PARAM2)
}
Log.d(TAG, "Fragment ${hashCode()} is created with ViewModel ${fragmentViewModel.hashCode()} injected.")
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
//Log.d(TAG, fragmentViewModel.toString())
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_blank1, container, false)
}
override fun onDestroy() {
super.onDestroy()
Log.d(TAG, "Fragment ${hashCode()} is being destroyed.")
}
companion object {
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* @param param1 Parameter 1.
* @param param2 Parameter 2.
* @return A new instance of fragment BlankFragment3.
*/
// TODO: Rename and change types and number of parameters
@JvmStatic
fun newInstance(param1: String, param2: String) =
BlankFragment1().apply {
arguments = Bundle().apply {
putString(ARG_PARAM1, param1)
putString(ARG_PARAM2, param2)
}
}
}
}
BlankFragment2.kt
package com.codelab.daniwebhiltviewmodels
import android.os.Bundle
import android.util.Log
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.viewModels
import dagger.hilt.android.AndroidEntryPoint
// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"
private const val TAG = "BLANK_FRAGMENT_2"
/**
* A simple [Fragment] subclass.
* Use the [BlankFragment2.newInstance] factory method to
* create an instance of this fragment.
*/
@AndroidEntryPoint
class BlankFragment2 : Fragment() {
// TODO: Rename and change types of parameters
private var param1: String? = null
private var param2: String? = null
private val fragmentViewModel: FragmentViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
param1 = it.getString(ARG_PARAM1)
param2 = it.getString(ARG_PARAM2)
}
Log.d(TAG, "Fragment ${hashCode()} is created with ViewModel ${fragmentViewModel.hashCode()} injected.")
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_blank2, container, false)
}
override fun onDestroy() {
super.onDestroy()
Log.d(TAG, "Fragment ${hashCode()} is being destroyed.")
}
companion object {
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* @param param1 Parameter 1.
* @param param2 Parameter 2.
* @return A new instance of fragment BlankFragment2.
*/
// TODO: Rename and change types and number of parameters
@JvmStatic
fun newInstance(param1: String, param2: String) =
BlankFragment2().apply {
arguments = Bundle().apply {
putString(ARG_PARAM1, param1)
putString(ARG_PARAM2, param2)
}
}
}
}
BlankFragment3.kt
package com.codelab.daniwebhiltviewmodels
import android.os.Bundle
import android.util.Log
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.activityViewModels
import dagger.hilt.android.AndroidEntryPoint
// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"
private const val TAG = "BLANK_FRAGMENT_3"
/**
* A simple [Fragment] subclass.
* Use the [BlankFragment3.newInstance] factory method to
* create an instance of this fragment.
*/
@AndroidEntryPoint
class BlankFragment3 : Fragment() {
// TODO: Rename and change types of parameters
private var param1: String? = null
private var param2: String? = null
private val fragmentViewModel: FragmentViewModel by activityViewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
param1 = it.getString(ARG_PARAM1)
param2 = it.getString(ARG_PARAM2)
}
Log.d(TAG, "Fragment ${hashCode()} is created with ViewModel ${fragmentViewModel.hashCode()} injected.")
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_blank3, container, false)
}
override fun onDestroy() {
super.onDestroy()
Log.d(TAG, "Fragment ${hashCode()} is being destroyed.")
}
companion object {
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* @param param1 Parameter 1.
* @param param2 Parameter 2.
* @return A new instance of fragment BlankFragment3.
*/
// TODO: Rename and change types and number of parameters
@JvmStatic
fun newInstance(param1: String, param2: String) =
BlankFragment3().apply {
arguments = Bundle().apply {
putString(ARG_PARAM1, param1)
putString(ARG_PARAM2, param2)
}
}
}
}
MyApplication.kt
package com.codelab.daniwebhiltviewmodels
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class ExampleApplication : Application()
**activity_main.xml**
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/button_toFragment1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/to_fragment_1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button_toFragment2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/to_fragment_2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button_toFragment1" />
<Button
android:id="@+id/button_toFragment3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/to_fragment_3"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button_toFragment2" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragmentContainerView"
android:name="com.codelab.daniwebhiltviewmodels.BlankFragment1"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/button_toFragment3" />
</androidx.constraintlayout.widget.ConstraintLayout>
fragment_blank_1.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".BlankFragment1">
<!-- TODO: Update blank fragment layout -->
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="@string/hello_blank_fragment" />
</FrameLayout>
fragment_blank_2.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".BlankFragment2">
<!-- TODO: Update blank fragment layout -->
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="@string/hello_blank_fragment2" />
</FrameLayout>
fragment_blank_3.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".BlankFragment3">
<!-- TODO: Update blank fragment layout -->
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="@string/hello_blank_fragment3" />
</FrameLayout>
strings.xml
<resources>
<string name="app_name">Daniweb Hilt ViewModels</string>
<string name="to_fragment_1">To Fragment 1</string>
<string name="to_fragment_2">To Fragment 2</string>
<string name="to_fragment_3">To Fragment 3</string>
<string name="hello_blank_fragment">Hello blank fragment</string>
<string name="hello_blank_fragment2">Hello blank fragment2</string>
<string name="hello_blank_fragment3">Hello blank fragment3</string>
</resources>
Project build.gradle
buildscript {
dependencies {
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.40.5'
}
}
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '7.1.1' apply false
id 'com.android.library' version '7.1.1' apply false
id 'org.jetbrains.kotlin.android' version '1.6.10' apply false
}
task clean(type: Delete) {
delete rootProject.buildDir
}
Module build.gradle
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
android {
compileSdk 32
defaultConfig {
applicationId "com.codelab.daniwebhiltviewmodels"
minSdk 21
targetSdk 32
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation "com.google.dagger:hilt-android:2.40.5"
implementation "androidx.fragment:fragment-ktx:1.4.1"
kapt "com.google.dagger:hilt-compiler:2.40.5"
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.codelab.daniwebhiltviewmodels">
<application
android:name=".ExampleApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.DaniwebHiltViewModels">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Summary
We have learned how to inject ViewModels with Hilt in this tutorial. The full project code can be found at https://github.com/dmitrilc/DaniwebHiltViewModels