Rain
Rain

Reputation: 73

Android ROOM, insert won't return ID with first insert, but will return with 2nd insert onwards

School project and I'm pretty new to Android development.

The problem

I have a button with a onClick listener in a save person fragment which will save person data to the database. Everything works fine except for some reason with the first click it wont return me the inserted row ID but it will do so with 2nd click onwards.

I really need this ID in before proceeding to the next fragment.

Not sure if this is important but whenever I return (reload) to this save person fragment, the behaviour is always the same that the first click allways fails to capture the inserted row ID.

input data:

first name = John
last name = Smith

Just for demo purpose, if I will try to use this button 3x to insert the person data (returned insert ID is in the log), I will get all 3 rows in database with name John Smith, but the very first inserted row ID is not captured (default initialised value is 0), please see the log below:

Log

2020-10-19 12:49:20.320 25927-25927/ee.taltech.mobile.contacts D/TEST_ADD_PERSON_ID: insertedPersonId: 0
2020-10-19 12:49:40.153 25927-25927/ee.taltech.mobile.contacts D/TEST_ADD_PERSON_ID: insertedPersonId: 5
2020-10-19 12:49:40.928 25927-25927/ee.taltech.mobile.contacts D/TEST_ADD_PERSON_ID: insertedPersonId: 6

EDITED ORIGINAL post

As suggested in the comments, I'm trying to go about the way of using LiveData and observer, but I'm still little bit stuck.

The setup

The below is the current setup.

Entity

@Entity(tableName = "person")
data class Person(

    @PrimaryKey(autoGenerate = true)
    val id: Int,

DAO

@Dao
interface PersonDao {

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun addPerson(person: Person): Long

Repository

class PersonRepository(private val personDao: PersonDao) {

    val readAllPersonData: LiveData<List<Person>> = personDao.readAllPersonData()

    suspend fun addPerson(person: Person): Long {
        return personDao.addPerson(person)
    }

ViewModel

I'm not sure if I'm doing things right at all here. I broke it down here in steps and created separate variables insertedPersonILiveData and insertedPersonId. How could pass the returned row id to insertedPersonILiveData?

class PersonViewModel(application: Application) : AndroidViewModel(application) {
        var insertedPersonILiveData: LiveData<Long> = MutableLiveData<Long>()
        var insertedPersonId: Long = 0L
    
        val readAllPersonData: LiveData<List<Person>>
        private val repository: PersonRepository
    
        init {
            val personDao = ContactDatabase.getDatabase(application).personDao()
            repository = PersonRepository(personDao)
            readAllPersonData = repository.readAllPersonData
        }
    
        suspend fun addPerson(person: Person) = viewModelScope.launch {
            insertedPersonId = repository.addPerson(person)
        // ****************************************************************       
        // insertedPersonILiveData = insertedPersonId (what to do here) ???
        // ****************************************************************       
        }

Save person fragment This is the way I'm calling out the addPerson via modelView.

val person = Person(0, firstName, lastName)

lifecycleScope.launch {
    personViewModel.addPerson(person)
}
Log.d("TEST_ADD_PERSON_ID","insertedPersonId: ${personViewModel.insertedPersonId}")

And this is the way I have done the observer (not sure if it's even correct).

val returnedIdListener: LiveData<Long> = personViewModel.insertedPersonILiveData
returnedIdListener.observe(viewLifecycleOwner, Observer<Long> { id: Long ->
    goToAddContactFragment(id)
})


private fun goToAddContactFragment(id: Long) {
    Log.d("TEST_ADD_PERSON_ID", "id: " + id)
}

Create database

@Database(
    entities = [Person::class, Contact::class, ContactType::class],
    views = [ContactDetails::class],
    version = 1,
    exportSchema = false
)
abstract class ContactDatabase : RoomDatabase() {

    abstract fun personDao(): PersonDao
    abstract fun contactTypeDao(): ContactTypeDao
    abstract fun contactDao(): ContactDao
    abstract fun contactDetailsDao(): ContactDetailsDao

    companion object {

        // For Singleton instantiation
        @Volatile
        private var instance: ContactDatabase? = null

        fun getDatabase(context: Context): ContactDatabase {
            return instance ?: synchronized(this) {
                instance ?: buildDatabase(context).also { instance = it }
            }
        }

        private fun buildDatabase(context: Context): ContactDatabase {
            return Room.databaseBuilder(context, ContactDatabase::class.java, "contacts_database")
                .addCallback(
                    object : RoomDatabase.Callback() {
                        override fun onCreate(db: SupportSQLiteDatabase) {
                            super.onCreate(db)
                            val request = OneTimeWorkRequestBuilder<SeedDatabaseWorker>().build()
                            WorkManager.getInstance(context).enqueue(request)
                        }
                    }
                )
                .build()
        }
    }
}

Upvotes: 4

Views: 1934

Answers (1)

cactustictacs
cactustictacs

Reputation: 19524

You're starting a coroutine to run addPerson, and then immediately calling Log with the current value of insertedPersonId in the viewmodel. The coroutine will run, insert the person, and update the VM with the ID of the inserted row, but that will happen long after your Log has run. Probably all of your results are actually the ID of the last record that was inserted.

I'm new to a lot of this too, but just based on what you have now, I think you just need to add

insertedPersonILiveData.value = insertedPersonId

in your addPerson function. That way you're updating that LiveData with a new value, which will be pushed to any valid observers. You've written some code that's observing that LiveData instance, so it should get the update when you set it.


edit your problem is that insertedPersonILiveData is the immutable LiveData type, so you can't set the value on it - it's read-only. You're creating a MutableLiveData object but you're exposing it as a LiveData type.

The recommended pattern for this is to create the mutable one as an internal object, expose a reference to it as an immutable type, and create a setter method that changes the value through the mutable reference (which it can access internally)

class myViewModel : ViewModel() {
    // mutable version is private, all updates go through the setter function
    // (the _ prefix is a convention for "private versions" of data fields)
    private val _lastInsertedPersonId = MutableLiveData<Long>()
  
    // we're making the instance accessible (for observing etc), but as
    // the immutable LiveData supertype that doesn't allow setting values
    val lastInsertedPersonId: LiveData<Long> = _lastInsertedPersonId

    // setting the value on the MutableLiveData instance
    // is done through this public function
    fun setLastInsertedPersonId(id: Long) {
        _lastInsertedPersonId.value = id
    }
}

and then your observer would just call lastInsertedPersonId.observe, you don't need to copy the LiveData and observe that (like you're doing with returnedIdListener.

That's the basic pattern right there - internal MutableLiveData, exposed publicly as an immutable LiveData val, with a setter method to update the value. Everything outside the view model either observes the LiveData that's visible, or calls the setter method to update. Hope that makes sense! It's not that complicated once you get your head around what's basically going on

Upvotes: 1

Related Questions