Reputation: 536
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
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
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
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