I have a problem building a second viewModel for my app.
In my project, the first viewModel observes a list of Contact, while the second viewModel observes a single contact.
Both viewModels are really similar, but the first builds, while the second cannot construct
E/AndroidRuntime: FATAL EXCEPTION: main
Process: fr.dleurs.android.contactapp, PID: 12324
java.lang.RuntimeException: Unable to start activity ComponentInfo{fr.dleurs.android.contactapp/fr.dleurs.android.contactapp.ui.detailsContact.DetailsContactActivity}: java.lang.IllegalArgumentException: Unable to construct viewmodel
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3449)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3601)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:223)
at android.app.ActivityThread.main(ActivityThread.java:7656)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
Caused by: java.lang.IllegalArgumentException: Unable to construct viewmodel
at fr.dleurs.android.contactapp.viewmodel.ContactViewModel$Factory.create(ContactViewModel.kt:33)
at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.java:187)
at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.java:150)
at fr.dleurs.android.contactapp.ui.detailsContact.DetailsContactActivity.onCreate(DetailsContactActivity.kt:40)
at android.app.Activity.performCreate(Activity.java:8000)
at android.app.Activity.performCreate(Activity.java:7984)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1309)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3422)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3601)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:223)
at android.app.ActivityThread.main(ActivityThread.java:7656)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
I/Process: Sending signal. PID: 12324 SIG: 9
The second viewModel : ContactViewModel.kt
package fr.dleurs.android.contactapp.viewmodel
import android.app.Application
import androidx.lifecycle.*
import fr.dleurs.android.contactapp.database.ContactDatabase
import fr.dleurs.android.contactapp.database.ContactsDatabase.Companion.getInstance
import fr.dleurs.android.contactapp.model.Contact
import fr.dleurs.android.contactapp.repository.ContactRepository
import kotlinx.coroutines.launch
class ContactViewModel(application: Application) : AndroidViewModel(application) {
private val contactsRepository = ContactRepository(getInstance(application).contactDtbDao())
fun liveContact(contactId: String): LiveData<Contact> = contactsRepository.contact(contactId)
public fun updateContact(contact: ContactDatabase) {
viewModelScope.launch {
contactsRepository.updateContact(contact);
}
}
public fun deleteContact(contactId: String) {
viewModelScope.launch {
contactsRepository.deleteContact(contactId);
}
}
class Factory(val app: Application) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(ContactsViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return ContactViewModel(app) as T
}
throw IllegalArgumentException("Unable to construct viewmodel")
}
}
}
The second activity, DetailsContactActivity.kt
package fr.dleurs.android.contactapp.ui.detailsContact
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.widget.ImageButton
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import fr.dleurs.android.contactapp.R
import fr.dleurs.android.contactapp.database.ContactDatabase
import fr.dleurs.android.contactapp.databinding.DetailsContactActivityBinding
import fr.dleurs.android.contactapp.model.Contact
import fr.dleurs.android.contactapp.model.asDatabaseModel
import fr.dleurs.android.contactapp.ui.main.createContactActivityRequestCode
import fr.dleurs.android.contactapp.ui.main.detailContactActivityRequestCode
import fr.dleurs.android.contactapp.ui.newModifyContact.CreateModifyContactActivity
import fr.dleurs.android.contactapp.viewmodel.ContactViewModel
import fr.dleurs.android.contactapp.viewmodel.ContactsViewModel
import timber.log.Timber
class DetailsContactActivity : AppCompatActivity() {
private lateinit var binding: DetailsContactActivityBinding
private lateinit var contactId: String
private lateinit var viewModel: ContactViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
contactId = intent?.getStringExtra("contactId") ?: ""
assert(!contactId.isNullOrEmpty())
viewModel = ViewModelProvider(
this,
ContactViewModel.Factory(this.application)
).get(ContactViewModel::class.java)
val buttonEdit = findViewById<ImageButton>(R.id.ibEdit)
val buttonDelete = findViewById<ImageButton>(R.id.ibDelete)
val buttonBack = findViewById<ImageButton>(R.id.ibBack)
binding = DataBindingUtil.setContentView(this, R.layout.details_contact_activity)
viewModel.liveContact(contactId).observe(this, Observer<Contact> { theContact -> // this or viewLifecycleOwner ?
theContact?.let {
binding.apply { contact = it }
}
})
buttonEdit.setOnClickListener {
val editIntent = Intent(this, CreateModifyContactActivity::class.java)
//editIntent.putExtra("contact", it)
startActivityForResult(editIntent, createContactActivityRequestCode)
}
buttonDelete.setOnClickListener {
viewModel.deleteContact(contactId)
finish()
}
buttonBack.setOnClickListener {
val replyIntent = Intent()
setResult(Activity.RESULT_CANCELED, replyIntent)
finish()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, intentData: Intent?) {
if (requestCode == createContactActivityRequestCode && resultCode == Activity.RESULT_OK ) {
val modifiedIntent = Intent()
val modifiedContact = intentData?.getParcelableExtra<ContactDatabase>("contact")
Timber.i("modifiedContact : " + modifiedContact.toString())
val contact: Contact = Contact(
id = modifiedContact!!.id.toString(),
firstName = modifiedContact!!.firstName,
lastName = modifiedContact!!.lastName,
mail = modifiedContact!!.mail
)
modifiedIntent.putExtra("action", "modify")
modifiedIntent.putExtra("contact", contact)
setResult(Activity.RESULT_OK, modifiedIntent)
finish()
}
super.onActivityResult(requestCode, resultCode, intentData)
}
}
The repository : ContactRepository.kt
package fr.dleurs.android.contactapp.repository
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import fr.dleurs.android.contactapp.database.*
import fr.dleurs.android.contactapp.model.Contact
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber
import fr.dleurs.android.contactapp.network.asDatabaseModel
import java.util.logging.Level.INFO
class ContactRepository(private val contactDtbDao: ContactDtbDao) {
val contacts: LiveData<List<Contact>> =
Transformations.map(contactDtbDao.getContacts()) {
it.asDomainModel()
}
fun contact(contactId: String): LiveData<Contact> =
Transformations.map(contactDtbDao.getContact(contactId)) { it.asDomailModel() }
suspend fun refreshContacts() {
withContext(Dispatchers.IO) {
Log.i("ContactRepo", "Refresh contacts is called");
val contactList = ContactRetrofitApi.contacts.getContacts()
Log.i("ContactRepo", "ContactList created ${contactList.toString()}");
contactDtbDao.insertAll(contactList.asDatabaseModel())
}
}
suspend fun insertContact(contact: ContactDatabase) {
withContext(Dispatchers.IO) {
contactDtbDao.insert(contact);
}
}
suspend fun deleteContact(contactId: String) {
withContext(Dispatchers.IO) {
contactDtbDao.delete(contactId);
}
}
suspend fun updateContact(contact: ContactDatabase) {
withContext(Dispatchers.IO) {
contactDtbDao.update(contact);
}
}
}
The working ViewModel, ContactsViewModel.kt :
package fr.dleurs.android.contactapp.viewmodel
import android.app.Application
import android.util.Log
import androidx.lifecycle.*
import fr.dleurs.android.contactapp.database.ContactDatabase
import fr.dleurs.android.contactapp.database.ContactsDatabase
import fr.dleurs.android.contactapp.model.Contact
import fr.dleurs.android.contactapp.repository.ContactRepository
import kotlinx.coroutines.launch
import java.io.IOException
class ContactsViewModel(application: Application) : AndroidViewModel(application) {
//private val contactsRepository = ContactRepository(ContactsDatabase.getDatabase(application))
private val contactsRepository = ContactRepository(ContactsDatabase.getInstance(application).contactDtbDao())
var liveContacts: LiveData<List<Contact>> = contactsRepository.contacts
public fun insertContact(contact: ContactDatabase) {
viewModelScope.launch {
contactsRepository.insertContact(contact);
}
}
/**
* Event triggered for network error. This is private to avoid exposing a
* way to set this value to observers.
*/
private var _eventNetworkError = MutableLiveData<Boolean>(false)
/**
* Event triggered for network error. Views should use this to get access
* to the data.
*/
val eventNetworkError: LiveData<Boolean>
get() = _eventNetworkError
/**
* Flag to display the error message. This is private to avoid exposing a
* way to set this value to observers.
*/
private var _isNetworkErrorShown = MutableLiveData<Boolean>(false)
/**
* Flag to display the error message. Views should use this to get access
* to the data.
*/
val isNetworkErrorShown: LiveData<Boolean>
get() = _isNetworkErrorShown
/**
* init{} is called immediately when this ViewModel is created.
*/
init {
refreshDataFromRepository()
}
/**
* Refresh data from the repository. Use a coroutine launch to run in a
* background thread.
*/
private fun refreshDataFromRepository() {
viewModelScope.launch {
try {
contactsRepository.refreshContacts()
_eventNetworkError.value = false
_isNetworkErrorShown.value = false
} catch (networkError: IOException) {
// Show a Toast error message and hide the progress bar.
Log.i("ContactViewModel", "Error on refreshContact : ${networkError.toString()}")
if (liveContacts.value.isNullOrEmpty())
_eventNetworkError.value = true
}
}
}
/**
* Resets the network error flag.
*/
fun onNetworkErrorShown() {
_isNetworkErrorShown.value = true
}
class Factory(val app: Application) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(ContactsViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return ContactsViewModel(app) as T
}
throw IllegalArgumentException("Unable to construct viewmodel")
}
}
}
The working activity, ContactActivity.kt
package fr.dleurs.android.contactapp.ui.main
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider
import androidx.viewpager.widget.ViewPager
import com.google.android.material.tabs.TabLayout
import fr.dleurs.android.contactapp.R
import fr.dleurs.android.contactapp.database.ContactDatabase
import fr.dleurs.android.contactapp.model.Contact
import fr.dleurs.android.contactapp.model.asDatabaseModel
import fr.dleurs.android.contactapp.ui.detailsContact.DetailsContactActivity
import fr.dleurs.android.contactapp.ui.newModifyContact.CreateModifyContactActivity
import fr.dleurs.android.contactapp.utils.FabButtonInterface
import fr.dleurs.android.contactapp.viewmodel.ContactsViewModel
import timber.log.Timber
public val createContactActivityRequestCode = 1
public val detailContactActivityRequestCode = 2
class ContactActivity : AppCompatActivity(), FabButtonInterface, OnClick {
private lateinit var viewModel: ContactsViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(
this,
ContactsViewModel.Factory(this.application)
).get(ContactsViewModel::class.java)
setContentView(R.layout.contact_activity)
val sectionsPagerAdapter =
SectionsPagerAdapter(context = this, fm = supportFragmentManager, onClick = this)
val viewPager: ViewPager = findViewById(R.id.contentFragment)
viewPager.adapter = sectionsPagerAdapter
val tabs: TabLayout = findViewById(R.id.tabs)
tabs.setupWithViewPager(viewPager)
}
override fun goToCreateContactActivity() {
Timber.i("Create a new contact started")
val intent = Intent(this, CreateModifyContactActivity::class.java)
startActivityForResult(intent, createContactActivityRequestCode)
}
fun goToDetailContactActivity(contactId: String) {
Timber.i("Details contact started")
val intent = Intent(this, DetailsContactActivity::class.java)
intent.putExtra("contactId", contactId)
startActivity(intent)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, intentData: Intent?) {
super.onActivityResult(requestCode, resultCode, intentData)
if (requestCode == createContactActivityRequestCode && resultCode == Activity.RESULT_OK) {
Timber.i("Intent received")
val response = intentData?.getParcelableExtra<ContactDatabase>("contact")
val newContact = ContactDatabase(
firstName = response!!.firstName,
lastName = response!!.lastName,
mail = response!!.mail
)
Timber.i("Intent received + " + newContact.toString())
viewModel.insertContact(newContact)
} else if (requestCode == createContactActivityRequestCode && resultCode == Activity.RESULT_CANCELED) {
} else if (requestCode == detailContactActivityRequestCode) {
Timber.i("DetailsContactActivity intent :" + intent.toString() + ", intentData: "+intentData.toString()+"resultCode :" + resultCode.toString())
if (resultCode == Activity.RESULT_CANCELED) {
Timber.i("DetailsContactActivity closed with no action")
}
} else {
Toast.makeText(this, "Error creating contact", Toast.LENGTH_LONG).show()
}
}
override fun onItemClick(contactId: String) {
Timber.i("On Item clicked + " + contactId)
goToDetailContactActivity(contactId)
}
}
Thank you for your help !
here you are passing ContactsViewModel::class.java
. You have to pass ContactViewModel::class.java
class Factory(val app: Application) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(ContactViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return ContactViewModel(app) as T
}
throw IllegalArgumentException("Unable to construct viewmodel")
}
}