Joe Pleavin
Joe Pleavin

Reputation: 536

Kotlin Coroutines - cannot return object from room db

I'm not super sure what I'm doing here so go easy on me:

I'm making a wordle clone and the word that is to be guessed is stored as a string in a pre-populated room database which I am trying to retrieve to my ViewModel and currently getting:

"StandaloneCoroutine{Active}@933049a"

instead of the actual data.

I have tried using LiveData which only returned null which as far as I'm aware is because it was not observed.

Switched to coroutines which seemed to make more sense if my UI doesn't need the data anyway. I ended up with this so far:

DAO:

@Dao
interface WordListDao {
    @Query("SELECT word FROM wordlist WHERE used = 0 ORDER BY id DESC LIMIT 1")
    suspend fun readWord(): String

// tried multiple versions here only string can be converted from Job

//  @Query("SELECT * FROM wordlist WHERE used = 0 ORDER BY id DESC LIMIT 1")
//  fun readWord(): LiveData<WordList>

//  @Query("SELECT word FROM wordlist WHERE used = 0 ORDER BY id DESC LIMIT 1")
//  fun readWord(): WordList
}

repository:

class WordRepository(private val wordListDao: WordListDao) {

    //val readWordData: String = wordListDao.readWord()

    suspend fun readWord(): String {
       return wordListDao.readWord()
    }
}

model:

@Entity(tableName = "wordlist")
data class WordList(
    @PrimaryKey(autoGenerate = true)
    val id: Int,
    val word: String,
    var used: Boolean
)

VM:

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

    private val repository: WordRepository
    private var word: String

    init {
        val wordDb = WordListDatabase.getDatabase(application)
        val wordDao = wordDb.wordlistDao()
        repository = WordRepository(wordDao)
        word = viewModelScope.launch {
                repository.readWord()
            }.toString()
        Log.d("TAG", ": $word") // does nothing?
    }
    println(word) // StandaloneCoroutine{Active}@933049a
}

This is the only way that I have managed to not get the result of:

Cannot access database on the main thread

There is a better way to do this, I just can't figure it out.

Upvotes: 0

Views: 1146

Answers (3)

Arpit Shukla
Arpit Shukla

Reputation: 10493

You can access the return value of repository.readWord() only inside the launch block.

viewModelScope.launch {
    val word = repository.readWord() 
    Log.d("TAG", ": $word")           // Here you will get the correct word
}

If you need to update you UI when this word is fetched from database, you need to use an observable data holder like a LiveData or StateFlow.

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

    private val repository: WordRepository
    private val _wordFlow = MutableStateFlow("")  // A mutable version for use inside ViewModel
    val wordFlow = _word.asStateFlow()            // An immutable version for outsiders to read this state

    init {
        val wordDb = WordListDatabase.getDatabase(application)
        val wordDao = wordDb.wordlistDao()
        repository = WordRepository(wordDao)
        viewModelScope.launch {
            _wordFlow.value = repository.readWord()
        }
    }
}

You can collect this Flow in your UI layer,

someCoroutineScope {
    viewModel.wordFlow.collect { word ->
        // Update UI using this word
    }
}

Edit: Since you don't need the word immediately, you can just save the word in a simple global variable for future use, easy.

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

    private lateinit var repository: WordRepository
    private lateinit var word: String

    init {
        val wordDb = WordListDatabase.getDatabase(application)
        val wordDao = wordDb.wordlistDao()
        repository = WordRepository(wordDao)
        viewModelScope.launch {
            word = repository.readWord()
        }
        // word is not available here, but you also don't need it here
    }

    // This is the function which is called when user types a word and presses enter
    fun submitGuess(userGuess: String) {
        // You can access the `word` here and compare it with `userGuess`
    }
}

The database operation will only take a few milliseconds to complete so you can be sure that by the time you actually need that original word, it will have been fetched and stored in the word variable.

Upvotes: 2

Tenfour04
Tenfour04

Reputation: 93541

(Now that I'm at a computer I can write a bit more.)

The problems with your current code:

  • You cannot safely read from the database on the main thread synchronously. That's why the suspend keyword would be used in your DAO/repository. Which means, there is no way you can have a non-nullable word property in your ViewModel class that is initialized in an init block.

  • Coroutines are asychronous. When you call launch, it is queuing up the coroutine to start its work, but the launch function returns a Job, not the result of the coroutine, and your code beneath the launch call continues on the same thread. The code inside the launch call is sent off to the coroutines system to be run and suspend calls will in most cases, as in this case, be switching to background threads back and forth. So when you call toString() on the Job, you are just getting a String representation of the coroutine Job itself, not the result of its work.

  • Since the coroutine does its work asynchronously, when you try to log the result underneath the launch block, you are logging it before the coroutine has even had a chance to fetch the value yet. So even if you had assigned the result of the coroutine to some String variable, it would still be null by the time you are logging it.

For your database word to be usable outside a coroutine, you need to put it in something like a LiveData or SharedFlow so that other places in code can subscribe to it and do something with the value when it arrives.

SharedFlow is a pretty big topic to learn, so I'll just use LiveData for the below samples.

One way to create a LiveData using your suspend function to retrieve the word is to use the liveData builder function, which returns a LiveData that uses a coroutine under the hood to get the value to publish via the LiveData:

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

    private val repository: WordRepository = WordListDatabase.getDatabase(application)
        .wordDb.wordlistDao()
        .let(::WordRepository)

    private val word: LiveData<String> = liveData {
        repository.readWord()
    }

    val someLiveDataForUi: LiveData<Something> = Transformations.map(word) { word ->
        // Do something with word and return result. The UI code can
        // observe this live data to get the result when it becomes ready.
    }
}

To do this in a way that is more similar to your code (just to help with understanding, since this is less concise), you can create a MutableLiveData and publish to the LiveData from your coroutine.

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

    private val repository: WordRepository
    private val word = MutableLiveData<String>()

    init {
        val wordDb = WordListDatabase.getDatabase(application)
        val wordDao = wordDb.wordlistDao()
        repository = WordRepository(wordDao)
        viewModelScope.launch {
            word.value = repository.readWord()
        }
    }

    val someLiveDataForUi: LiveData<Something> = Transformations.map(word) { word ->
        // Do something with word and return result. The UI code can
        // observe this live data to get the result when it becomes ready.
    }
}

If you're not ready to dive into coroutines yet, you can define your DAO to return a LiveData instead of suspending. It will start reading the item from the database and publish it through the live data once it's ready.

@Dao
interface WordListDao {
    @Query("SELECT word FROM wordlist WHERE used = 0 ORDER BY id DESC LIMIT 1")
    fun readWord(): LiveData<String>
}
class HomeViewModel(application: Application) : ViewModel() {

    private val repository: WordRepository = WordListDatabase.getDatabase(application)
        .wordDb.wordlistDao()
        .let(::WordRepository)

    private val word: LiveData<String> = repository.readWord()

    //...
}

Upvotes: 2

Daniel Knauf
Daniel Knauf

Reputation: 579

The return value is as expected, because launch does always return a Job object representing the background process.

I do not know how you want to use the String for, but all operations which should be done after receiving the String must be moved inside the Coroutine or in a function which is called from the Coroutine.

viewModelScope.launch {
                val word = repository.readWord()
                
                // do stuff with word
                
                // switch to MainThread if needed
                launch(Dispatchers.Main){}
            }

Upvotes: 1

Related Questions