Android Room+MVVM Part 2: Hilt, ClickEvent of RecyclerViewAdapter &. @ViewModelInject

Homan Huang
7 min readAug 4, 2020

--

< == Part One

At this point, you shall finish part 1 which is a non-clickable project. NPE (Null Point Exception) is a major problem for a new project. You will feel like a dummy to fix the same problem again and again. To solve that, you may inject the new instance by Dagger. But if you have installed Dagger dependency before, you will know how hard to use it. Modules, Scopes, Components, and Factory, are mess up in a same graph. Luckily, Android Studio 4.2 offers us Dagger navigation Hilt to ease our work. We can inject instances by a few lines of code.

— === Menu === —

🐲1. Install Android Studio > 4.2
🚞2. Setup Gradle for Hilt
💉3. @Inject with Hilt
🔛4. UserDetailFragment ⎌ UserViewModel
🐾5. ViewAdapter⇨MainActivity⇨Fragment
🏰6. Hilt ViewModel Support: @InjectViewModel
🐭7. Add Data to UserClickEvent

🐲1. Install Android Studio > 4.2

==> Menu

Staring Android Studio 4.1, you can add Hilt dependency. In 4.2, we can use the Dagger navigation. If you still have older IDE, let’s upgrade.

https://developer.android.com/studio/preview/

Download => Unzip => Click bin => run studio64.

🚞2. Setup Gradle for Hilt

==> Menu

The build.gradle(Project):

buildscript {
...
dependencies {
...

classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
}
}

ext {
// UI
appCompatVersion = '1.1.0'
recyclerViewVersion = '1.1.0'
cardViewVersion = '1.0.0'
constraintLayoutVersion = '1.1.3'
legacySupportVersion = '1.0.0'

// test
espressoVersion = '3.2.0'
junitVersion = '4.13'
androidx_test_version = '1.2.0'

// jetpack
lifecycleVersion = '2.2.0'
lifecycleExtensionsVersion = '2.2.0'
roomVersion = '2.2.5'
savedStateVersion = '2.2.0'

// kotlin
kotlinCore = '1.3.1'
kotlinVM = '2.2.0'
kotlinFragment = '1.2.5'

// rxjava
rxJavaV = '3.0.4'
rxAndroidV = '3.0.0'

// dagger hilt
dagger_version = '2.27'
hilt_jetpack_version = '1.0.0-alpha02'
hilt_version = '2.28-alpha'
}

The build.gradle(app):

...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'

android {
...
kotlinOptions {
jvmTarget = "1.8"
}
}

dependencies {
implementation "androidx.appcompat:appcompat:$appCompatVersion"
implementation "androidx.legacy:legacy-support-v4:$legacySupportVersion"

// UI
implementation "androidx.cardview:cardview:$cardViewVersion"
implementation "androidx.recyclerview:recyclerview:$recyclerViewVersion"
implementation "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion"
implementation 'com.google.android.material:material:1.1.0'
implementation 'androidx.gridlayout:gridlayout:1.0.0'

// Lifecycle
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycleExtensionsVersion"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"

// Kotlin
implementation "androidx.core:core-ktx:$kotlinCore"
implementation "androidx.fragment:fragment-ktx:$kotlinFragment"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$kotlinVM"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

// RxJava
implementation "io.reactivex.rxjava3:rxandroid:$rxAndroidV"
implementation "io.reactivex.rxjava3:rxjava:$rxJavaV"

// Room (use 1.1.0-beta2 for latest beta)
implementation "androidx.room:room-runtime:$roomVersion"
kapt "androidx.room:room-compiler:$roomVersion"
// RxJava support for Room
implementation "androidx.room:room-rxjava2:$roomVersion"

// Test
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test:runner:$androidx_test_version"
androidTestImplementation "androidx.test:core-ktx:$androidx_test_version"
testImplementation "androidx.room:room-testing:$roomVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
kaptAndroidTest "com.google.dagger:dagger-compiler:$dagger_version"

// Dagger dependencies
implementation "com.google.dagger:dagger:$dagger_version"
kapt "com.google.dagger:dagger-compiler:$dagger_version"

// Hilt dependencies
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"

// Hilt ViewModel extension
implementation "androidx.hilt:hilt-lifecycle-viewmodel:$hilt_jetpack_version"
kapt "androidx.hilt:hilt-compiler:$hilt_jetpack_version"

// Hilt testing dependencies
androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
kaptAndroidTest "com.google.dagger:hilt-android-compiler:$hilt_version"
kaptAndroidTest "androidx.hilt:hilt-compiler:$hilt_jetpack_version"
}

I input the Hilt plugin and dependencies of Dagger and Hilt.

💉3. @Inject with Hilt

==> Menu

Dagger is not that hard to understand:

Module: It provides instance or constant data.
Component: It create bridge to View.
Consumer(@Inject): The place gets injected.

With Hilt, you may not need to write the Modules to provide instance except the constant data type; for example, the setting of RestApi.

Application

The first change is the application:

@HiltAndroidApp
class UserApplication : Application() {

@AndroidEntryPoint: Activity or Fragment

At MainActivity.kt, let’s inject the ViewModel.

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

// viewModel
@Inject
lateinit var userViewModel: UserViewModel

override fun onCreate(savedInstanceState: Bundle?) {
...

// viewModel
//lgd("create userViewModel")
//userViewModel = ViewModelProvider(this)
// .get(UserViewModel::class.java)

@AndroidEntryPoint: The instance of UserViewModel will inject at @Inject annotation of MainActivity.

@Inject Constructor

Next, at UserViewModel.kt,

class UserViewModel @Inject constructor(
private val userRepository: UserRepository) {

// user repository to get data.
//private val userRepository: UserRepository

@Inject: The instance of UserRepository will inject to UserViewModel.

@Singleton

Next, at UserRepository.kt,

@Singleton
class UserRepository @Inject constructor() {
...
companion object {
...
/*
fun getInstance(): UserRepository {
lgd ("getInstance()")
return UserRepository()
}
*/

}
}

@Singleton: It helps the instance to be created and used once across the app. Now, let’s run the app. Mine is working fine.

Hilt: Visual Navigation

With the Hilt plugin, you can visualize the Dagger connection.

This saves a lot of work of old Dagger. And it can easily spot the problem.

🔛4. UserDetailFragment + UserViewModel

==> Menu

In Main Folder, I stock the MainActivity.kt and UserViewModel.kt. Right-click, I add a new Fragment with ViewModel, called UserDetail+Fragment/ViewModel.

Like activity, let’s add @AndroidEntryPoint annotation to the fragment, and inject the ViewModel.

@AndroidEntryPoint
class UserDetailFragment : Fragment() {

@Inject
lateinit var viewModel: UserDetailViewModel
...
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
//viewModel = ViewModelProvider(this)...
}
...
}

UI: user_detail_fragment.xml

UI: FrameLayout(fragment container) in activity_main.xml.

At activity_main.xml,

...
<FrameLayout
android:id="@+id/user_frg"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/design_default_color_background"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

I have set background color so the new loaded fragment will not be looked as transparent. I hide its visibility so it won’t block the others.

MVVM: UserDetailViewModel.kt

Obviously, the Hilt style is much easier to write ViewModel.

class UserDetailViewModel@Inject constructor() {
var user: UserTable? = null
}

🐾5. ViewAdapter⇨MainActivity⇨Fragment

==> Menu

Let’s add click function to the UserViewAdapter.

First, in the main package, I add an interface for the click event, called UserClickEvent.kt.

Let’s test the click without any data.

At UserViewAdapter.kt,

class UserViewAdapter ...{
...

// Handle item click
private val mClickListener: UserClickEvent = context as UserClickEvent

...

override fun onBindViewHolder(
holder: UserViewHolder,
position: Int) {
val current = allUsers[position]
holder.userIdView.text = current!!.uid.toString()
holder.userIdView.setOnClickListener{
mClickListener.onItemClick()
}
holder.userNameView.text = current.name
holder.userNameView.setOnClickListener{
mClickListener.onItemClick()
}
}
...
}

Simple, I have not input any data, yet. Let’s use with empty. This adapter is adopting to MainActivity. Let’s add the click event, too.

class MainActivity : AppCompatActivity(), UserClickEvent {

Now, let’s implement the member to the MainActivity.kt.

override fun onItemClick() {
lgi("Item clicked!")
msg(this, "Item clicked!")

addUser_fab.visibility = View.GONE
user_frg.visibility = View.VISIBLE

supportFragmentManager
.beginTransaction()
.replace(R.id.user_frg, UserDetailFragment())
.addToBackStack(null)
.commit()
}
companion object {
// logcat
val tag = "MYLOG "+"MainActivity"
fun lgi(s: String) = Log.i(tag, s)
fun lgd(s: String) = Log.d(tag, s)
fun lge(s: String) = Log.e(tag, s)

// toast
fun msg(context: Context, s: String) =
Toast.makeText(context, s, LENGTH_LONG).show()
}

Of course, you need to manage the back-pressed, too.

override fun onBackPressed() {
this.supportFragmentManager.popBackStack()
user_frg.visibility = View.GONE
addUser_fab.visibility = View.VISIBLE
}

At UserDetailFragment.kt, let’s handle the back-pressed on back_fab button.

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)

back_fab.setOnClickListener{
activity?.onBackPressed()
}
}

Now, run the app without any data.

🏰6. Hilt ViewModel Support: @InjectViewModel

==> Menu

Furthermore, we can migrate to @ViewModelInject instead of @Inject. I found that it is much easier to input and less code. The drawback is that Hilt anchors will be gone. Let’s upgrade.

At MainActivity.kt,

@AndroidEntryPoint
class MainActivity : AppCompatActivity(), UserClickEvent {

// viewModel
private val userViewModel: UserViewModel by viewModels()
private val userDetailViewModel: UserDetailViewModel by viewModels()

No more @Inject.

At UserDetailFragment.kt,

@AndroidEntryPoint
class UserDetailFragment : Fragment() {

private val userDetailViewModel: UserDetailViewModel by activityViewModels()

The userDetalViewModel is shared in Activity and Fragment.

At UserViewModel.kt,

class UserViewModel @ViewModelInject constructor(
private val userRepository: UserRepository) : ViewModel() {

At UserDetailViewModel.kt,

class UserDetailViewModel @ViewModelInject constructor() : ViewModel()  {

It’s done. Test run and continue.

🐭7. Add Data to UserClickEvent

==> Menu

We can insert the data to UserClickEvent.kt.

interface UserClickEvent {
// item in UserViewAdapter
fun onItemClick(user: UserTable)
}

At UserViewAdapter.kt, let’s add current user.

override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
val current = allUsers[position]
holder.userIdView.text = current!!.uid.toString()
holder.userIdView.setOnClickListener {
mClickListener.onItemClick(current)

}
holder.userNameView.text = current.name
holder.userNameView.setOnClickListener {
mClickListener.onItemClick(current)

}
}

LiveData: MutableLiveData<>

Set up the LiveData on UserDetailViewModel.kt,

class UserDetailViewModel @ViewModelInject constructor() : ViewModel()  {
private var currentUser: MutableLiveData<UserTable>? = null

fun getUser( ): MutableLiveData<UserTable>? {
if (currentUser == null) {
currentUser = MutableLiveData()
}
return currentUser
}
}

At MainActivity.kt,

override fun onItemClick(data: UserTable) {
lgd("data: ${data.uid}")
userDetailViewModel.getUser()?.value = data ...
}

I also observe the currentUser on UserDetailFragment.kt.

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
...
// ViewModel injects Livedata
userDetailViewModel.getUser()!!.observe(viewLifecycleOwner,
Observer<UserTable> { user ->
if (user != null) {
lgd("User id: ${user.uid}")

user_id_tv.text = user.uid.toString()
user_name_tv.text = user.name
user_address_tv.text = user.address
} else {
context?.let { msg(it, "User Data: Empty!") }
}
})
}
companion object {
// logcat
val tag = "MYLOG "+"UserDetailFrg"
fun lgi(s: String) = Log.i(tag, s)
fun lgd(s: String) = Log.d(tag, s)
fun lge(s: String) = Log.e(tag, s)

// toast
fun msg(context: Context, s: String) =
Toast.makeText(context, s, Toast.LENGTH_LONG).show()
}

Let’s run.

Working fine!

You shall learn the easy Dagger — Hilt in this part.

In the next part, I will add the second activity to add user and RxJava to manage the Room.

See ya!

==> Part Three

--

--

Homan Huang
Homan Huang

Written by Homan Huang

Computer Science BS from SFSU. I studied and worked on Android system since 2017. If you are interesting in my past works, please go to my LinkedIn.

No responses yet