I'll try to make this my only question - how do I ...
# room
v
I'll try to make this my only question - how do I update the primary key, or return the newly generated table row, after an insert? Code samples in thread... ``````
Copy code
@Entity
data class Vehicle(val manufacturer: String, val model: String, val odometerReading: Int) {
    @PrimaryKey(autoGenerate = true)
    var id: Int = 0
}
When I do an insert, I really really want Room to return to me the newly created Vehicle - but the DAO will only return
Unit
or the SQLite
rowId
. I've tried multiple things, most recently this:
Copy code
class VehicleRepository(private val vehicleDao: VehicleDao) {
  suspend fun insert(vehicle: Vehicle) {
        val newVehicleRowId = vehicleDao.insert(vehicle)
        vehicle.id = vehicleDao.getVehicleWithRowId(newVehicleRowId)
    }
}
And even though I'm calling it from the ViewModel:
Copy code
fun insert(vehicle: Vehicle): LiveData<Vehicle> {
        val newVehicle = MutableLiveData<Vehicle>()
        viewModelScope.launch {
            repository.insert(vehicle)
            selectedVehicle.postValue(vehicle)
        }
        return newVehicle
    }
I get this awful error:
Copy code
java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.
In other ORMs, and other databases, I'd expect to get the
@PrimaryKey
from an insert operation.
I'm completely befuddled.... even if I move all the repository calls into the
viewModelScope.launch{}
block, I get the same error:
Copy code
fun insert(vehicle: Vehicle) {
        viewModelScope.launch {
            val rowId = repository.insert(vehicle)
            val pk = repository.getVehicleIdFromRowId(rowId)
            val newVehicle = repository.getVehicleById(pk)
            selectedVehicle.postValue(newVehicle.value)
            _getVehicleCount()
        }
    }
I have been through several Room tutorials, but they push me to playing with lists and Flows and so on too quickly, it seems, for my little brain.
p
Can you post your Dao and also the code where you create with RoomDatabase.Builder?
I would expect Room to take care of switching to an IO thread under the hood, but it's not doing that. Even if you can't figure that out, you can pass in additional Coroutine Context to make the coroutine perform the work off the main thread
Copy code
fun insert(vehicle: Vehicle): LiveData<Vehicle> {
        val newVehicle = MutableLiveData<Vehicle>()
        viewModelScope.launch(<http://Dispatchers.IO|Dispatchers.IO>) {
            repository.insert(vehicle)
            selectedVehicle.postValue(vehicle)
        }
        return newVehicle
    }
v
Thanks, I'll try in the morning. I feel like I take one step forwards and then two steps back each time I work on this project.
Database builder:
Copy code
@Database(entities = arrayOf(ChargeEvent::class, Vehicle::class), version = 5, exportSchema = false)
abstract class AmberDatabase : RoomDatabase() {

    abstract fun chargeEventDao(): ChargeEventDao
    abstract fun vehicleDao(): VehicleDao

    companion object {
        @Volatile
        private var INSTANCE: AmberDatabase? = null

        fun getDatabase(context: Context, scope: CoroutineScope) : AmberDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    AmberDatabase::class.java,
                    "amber_database")
                    .addCallback(AmberDatabaseCallback(scope)
                ).fallbackToDestructiveMigration().build()
                INSTANCE = instance
                instance
            }
        }
    }

    private class AmberDatabaseCallback(private val scope: CoroutineScope) : Callback() {
        override fun onCreate(db: SupportSQLiteDatabase) {
            super.onCreate(db)
            INSTANCE?.let { database ->
                scope.launch {
                    // populate database with fake data? Or other data setup tasks
                }
            }
        }
    }
}
Entity & DAO:
Copy code
@Entity
data class Vehicle(val manufacturer: String, val model: String, val odometerReading: Int) {
    @PrimaryKey(autoGenerate = true)
    var id: Int = 0
}

@Dao
interface VehicleDao {

    @Insert
    suspend fun insert(vehicle: Vehicle): Long

    @Delete
    suspend fun delete(vehicle: Vehicle)

    @Update
    suspend fun update(vehicle: Vehicle)

    @Query("SELECT * FROM Vehicle where id = :id LIMIT 1")
    fun getVehicle(id: Int): LiveData<Vehicle>

    @Query("SELECT COUNT(*) FROM Vehicle")
    fun getVehicleCount(): LiveData<Int>

    @Query("SELECT MAX(id) FROM Vehicle")
    fun getMostRecentVehicleId(): LiveData<Int>

    @Query("SELECT id FROM Vehicle WHERE rowId = :rowId")
    fun getVehiclePkWithRowId(rowId: Long): LiveData<Int>
}
Repository:
Copy code
class VehicleRepository(private val vehicleDao: VehicleDao) {

    val vehicleCount = vehicleDao.getVehicleCount()

    suspend fun getVehicleById(id: Int): LiveData<Vehicle> = vehicleDao.getVehicle(id)

    suspend fun insert(vehicle: Vehicle) = vehicleDao.insert(vehicle)
//        vehicle.id = vehicleDao.getVehicleWithRowId(newVehicleRowId)
//    }

    suspend fun getVehicleCount(): LiveData<Int> = vehicleDao.getVehicleCount()

    fun getVehicleIdFromRowId(rowId: Long): LiveData<Int> = vehicleDao.getVehiclePkWithRowId(rowId)

}
ViewModel (this one is a mess, I keep changing it):
Copy code
class VehicleDetailsViewModel(application: AmberApplication) : ViewModel() {

    private val repository: VehicleRepository = application.vehicleRepo

    private val _vehicleCount = MutableLiveData<Int>()
    val vehicleCount: LiveData<Int>
        get() = _vehicleCount

    init {
        _getVehicleCount()
    }


    var selectedVehicle: MutableLiveData<Vehicle> = MutableLiveData<Vehicle>()

    fun insert(vehicle: Vehicle) {
        viewModelScope.launch {
            val rowId = repository.insert(vehicle)
            val pk = repository.getVehicleIdFromRowId(rowId)
            val newVehicle = repository.getVehicleById(pk.value!!)
            selectedVehicle.postValue(newVehicle.value)
            _getVehicleCount()
        }
    }

    private fun _getVehicleCount() {
        viewModelScope.launch {
            val vCount = repository.getVehicleCount()
            _vehicleCount.value = vCount.value
        }
    }

    private fun getSelectedVehicle(id: Int): LiveData<Vehicle> = liveData {
        repository.getVehicleById(id)
    }
}
When I change it to
viewModelScope.launch(<http://Dispatchers.IO|Dispatchers.IO>)
I get an NPE at
val newVehicle = repository.getVehicleById(pk.value)
.
Part of the Composable function:
Copy code
@Composable
fun VehicleDetailsScreen(navController: NavController, viewModel: VehicleDetailsViewModel) {
    val context = LocalContext.current

    val totalVehicles by viewModel.vehicleCount.observeAsState()

// layout stuff here.... then
            if (totalVehicles == null || totalVehicles == 0) {
                AddVehicle(context, viewModel)
            } else {
                ShowCurrentVehicle(context, viewModel)
            }
        }
    }
}
Right now my two problems are: when I start the app there are no Vehicles, so it displays the
AddVehicle
layout. When I save, it should update the `vehicleCount`and recompose the layout, and it should select the new vehicle as the current vehicle to show it under
ShowCurrentVehicle
. But the `vehicleCount`doesn't seem to get updated, and neither does the selected vehicle - both are null by the time it reaches the composable function.
I'm nearly there but still struggling. I need to create
MutableLiveData
object with the ease of creating a
LiveData
object - because after it is created, I will need to update it:
Copy code
// in ViewModel...
private var _selectedVehicleId: LiveData<Long> = liveData {
        val vehicleId = repository.getMostRecentVehicleId()
        emit(vehicleId)
    }
fun insert(vehicle: Vehicle) {
        viewModelScope.launch(<http://Dispatchers.IO|Dispatchers.IO>) {
            val rowId = repository.insert(vehicle)
            _selectedVehicleId = rowId // // *** THIS LINE ISN'T ALLOWED BECAUSE _selectedVehicleId isn't Mutable. But I've no idea how to create a Mutable object from the result of a suspend function call like repository.getMostRecentVehicleId()
        }
    }
p
Copy code
private var _selectedVehicleId: MutableLiveData<Long> = MutableLiveData() 
init {
        viewModelScope.launch(<http://Dispatchers.IO|Dispatchers.IO>) {
               _selectedVehicle.postValue(repository.getMostRecentVehicleId())
    }
fun insert(vehicle: Vehicle) {
        viewModelScope.launch(<http://Dispatchers.IO|Dispatchers.IO>) {
            val rowId = repository.insert(vehicle)
            _selectedVehicleId.postValue(rowId) 
        }
    }
an init block in the viewmodel could help you here. As soon as the viewmodel is created, the init block will be called so almost immediately the mutable livedatawill have a value
v
I had the same thought, and after a few more tweaks it's working. But by golly I've still got a lot to learn.
Copy code
lateinit var selectedVehicle: LiveData<Vehicle?>

    private var _selectedVehicleId: MutableLiveData<Long> = MutableLiveData<Long>().also {
        viewModelScope.launch(<http://Dispatchers.IO|Dispatchers.IO>) {
            val vehicleId = repository.getMostRecentVehicleId()
            Log.e("ALSO block","mostRecentVehicleId = $vehicleId")
            it.postValue(vehicleId)
        }
    }

    init {
       viewModelScope.launch(<http://Dispatchers.IO|Dispatchers.IO>) {
           val mostRecentVehicleId = repository.getMostRecentVehicleId()
           Log.e("INIT block","mostRecentVehicleId: $mostRecentVehicleId")
           _selectedVehicleId.postValue(mostRecentVehicleId)
           selectedVehicle = repository.getVehicleById(mostRecentVehicleId)
           Log.e("INIT block","selectedVehicle.value.id: ${selectedVehicle.value?.id}")
       }
    }