Android – Retrieving in item from a Room database by its ID

Issue

I’m trying to retrieve a single Client item from my Room database. Every client is displayed in a list, and each client has an edit button on them. When the button is pressed, I would like to retrieve that client from the database by their id. Their details will then be displayed on an edit screen.

My problem arises in actually getting the client from the database. So far I have tried 2 approaches:

Coroutines based approach

I have tried to retrieve the item using coroutine based functions with Room. This approach does “work” to an extent, but in its current form the coroutine ends up retrieving the newly searched for client ***after*** the edit screen has been displayed. This makes it so that when you edit a client, you end up editing the one you tried to edit previously.

I have tried to counteract this by using .join(), using viewModelScope.async rather than launch and then attempting to use .await, and a few other ideas, but none of them have worked.

ClientDao.kt

@Dao
interface ClientDao {
    @Query("SELECT * FROM tblClient WHERE id = :id")
    suspend fun getClientToEdit(id: Int): List<Client>
}

ClientRepository.kt

class ClientRepository(private val clientDao: ClientDao) {

    val clientSearchResults = MutableLiveData<List<Client>>() 

    suspend fun getClientToEdit(id: Int) {
        clientSearchResults.value = clientDao.getClientToEdit(id)
    }
}

ClientViewModel.kt

class ClientViewModel(application: Application): ViewModel() {

    private val repository: ClientRepository
    val clientSearchResults: MutableLiveData<List<Client>>

    init {
        val clientDB = ManagementDatabase.getDatabase(application)
        val clientDao = clientDB.clientDao()
        repository = ClientRepository(clientDao)

        clientSearchResults = repository.clientSearchResults
    }

    fun getClientToEdit(clientId: Int) = viewModelScope.launch {
        repository.getClientToEdit(clientId)
    }

}

ManagementApp.kt

ClientScreen(
   onEditClient = { id ->
       clientViewModel.getClientToEdit(id)
           val editClientList: List<Client>? = clientViewModel.clientSearchResults.value
           //This looks awful but it works
           // It just gets the client details of the selected client
           if (editClientList != null) {
                if (editClientList.firstOrNull() != null) {
                     selectedClient = editClientList[0]

If I could just find a way to make it so that clientViewModel.getClientToEdit(id) fully executed before running the rest of the code in ManagementApp.kt, it would work. The problem is I’m not sure how.

Flow based approach:

I didn’t really think this approach would work, but it was worth a shot. I have tried to retrieve the item using a flow list, in the same way I have been retrieving the whole list.

ClientDao.kt

@Dao
interface ClientDao {
   @Query("SELECT * FROM tblClient WHERE id = :id")
   fun getClientToEdit(id: Int): Flow<List<Client>>
}

ClientRepository.kt

class ClientRepository(private val clientDao: ClientDao) {
    fun getClientSearchResults(id: Int): Flow<List<Client>> =
        clientDao.getClientToEdit(id)
}

ClientViewModel.kt

class ClientViewModel(application: Application): ViewModel() {
        private val repository: ClientRepository
    
        init {
            val clientDB = ManagementDatabase.getDatabase(application)
            val clientDao = clientDB.clientDao()
            repository = ClientRepository(clientDao)
        }
    
        fun getClientToEdit(clientId: Int): LiveData<List<Client>> {
            return repository.getClientSearchResults(id = clientId).asLiveData()
        }
    }

ManagementApp.kt

ClientScreen(
    onEditClient = { id ->
        val editClientList by clientViewModel.getClientToEdit(id).observeAsState(listOf())
        //This looks awful but it works
        // It just gets the client details of the selected client
        if (editClientList != null) {
             if (editClientList.firstOrNull() != null) {
                  selectedClient = editClientList[0]

The problem with this approach is that .observeAsState gives me the ‘@Composable invocations can only happen from the context of a @Composable function’ error (Although the snippet of code above is actually within a @Composable function).

If anyone could provide some much needed help I would greatly appreciate it. I’m new to Android and have struggled with Room quite a bit, so my apologies if the code isn’t really up to scratch. Thank you.

Solution

When the button is pressed, I would like to retrieve that client from the database by their id. Their details will then be displayed on an edit screen.

If by "edit screen" you mean proper screen with ViewModel to which you navigate using for example androidx.navigation, better approach would be to just pass the id to that new screen and do the loading in its ViewModel.

If I could just find a way to make it so that clientViewModel.getClientToEdit(id) fully executed before running the rest of the code in ManagementApp.kt

You can do that by making getClientToEdit suspend fun and then doing something like this:

val scope = rememberCoroutineScope()
ClientScreen(
    onEditClient = { id ->
        scope.launch {
            clientViewModel.getClientToEdit(id)
            // now getClientToEdit was executed
        }
    }
)

I would also suggest returning Client directly from the getClientToEdit, using LiveData for that is not necessary

Although the snippet of code above is actually within a @Composable function

It’s not, you are trying to call it from onClick callback and onClick is not marked with @Composable, so you cannot call composable functions from there.


To sum it up:
If the result of your action is navigation to another screen, you can do one of these:

  1. Pass just the id to that other screen and do the loading there, as I suggested.
  2. Launch coroutine inside onEditClient callback, load the client and navigate from there as shown above.
  3. Load the client in ViewModel, update some state there and navigate based on that state, something like:
// ViewModel
val actions = MutableSharedFlow<Action>()
fun editClient(id: Int) = viewModelScope.launch {
    val client = repository.getClientToEdit(clientId)
    actions.emit(NavigateToEditScreen(client))
}

// Screen
val action by clientViewModel.actions.collectAsState()
LaunchedEffect(action) {
    if (action is NavigateToEditScreen) {
        // do the navigation using action.client
    }
}
ClientScreen(
   onEditClient = { id ->
       clientViewModel.editClient(id)
   }
)

Answered By – Jan Bína

This Answer collected from stackoverflow, is licensed under cc by-sa 2.5 , cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply

(*) Required, Your email will not be published