Android Room+MVVM Part 2: Hilt, ClickEvent of RecyclerViewAdapter &. @ViewModelInject
< == 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.