Kotlin协程-不能从房间db返回对象



我不太清楚我在做什么,所以请原谅我:

我正在制作一个世界克隆,要猜的单词存储为一个字符串,在一个预填充的房间数据库中,我试图检索到我的ViewModel,目前得到:

"StandaloneCoroutine{Active}@933049a"

而不是实际数据

我已经尝试使用LiveData,它只返回null,据我所知,这是因为它没有被观察到。

切换到协程,如果我的UI不需要数据,这似乎更有意义。到目前为止,我的结果是:

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
}

存储库:

class WordRepository(private val wordListDao: WordListDao) {
//val readWordData: String = wordListDao.readWord()
suspend fun readWord(): String {
return wordListDao.readWord()
}
}

模型:

@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
}

这是唯一的方法,我已经设法不得到的结果:

Cannot access database on the main thread

有更好的方法,我就是想不出来

只能在启动块内部访问repository.readWord()的返回值。

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

如果你需要在从数据库中获取这个词时更新你的UI,你需要使用一个可观察的数据持有人,如LiveDataStateFlow

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()
}
}
}

你可以收集这个流在你的UI层,

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

编辑:由于您不需要立即使用该单词,因此您可以将该单词保存在一个简单的全局变量中以备将来使用,非常简单。

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`
}
}

数据库操作只需要几毫秒就可以完成,因此您可以确定,当您实际需要原始单词时,它将被获取并存储在word变量中。

(现在我在电脑前,我可以多写一点)

当前代码的问题:

  • 不能安全地从主线程上同步读取数据库。这就是为什么要在DAO/存储库中使用suspend关键字。这意味着,没有办法你可以有一个非空的word属性在你的ViewModel类是初始化的init块。

  • 协程是异步的。当您调用launch时,它正在排队等待协程开始它的工作,但是launch函数返回一个Job,而不是协程的结果,并且在launch调用下的代码继续在同一个线程上运行。launch调用中的代码被发送到协程系统中运行,在大多数情况下,挂起调用将来回切换到后台线程,就像在本例中一样。因此,当您在Job上调用toString()时,您只是获得协程Job本身的字符串表示形式,而不是其工作的结果。

  • 由于协程异步地完成其工作,当您尝试在launch块下面记录结果时,您在协程甚至有机会获取值之前就记录了它。因此,即使您将协程的结果赋值给某个String变量,当您记录它时,它仍然是空的。

为了使你的数据库字在协程之外可用,你需要把它放在像LiveData或SharedFlow这样的东西中,以便代码中的其他地方可以订阅它,并在它到达时对值做一些事情。

SharedFlow是一个非常大的主题,所以我只使用LiveData作为下面的示例。

使用挂起函数创建LiveData来检索单词的一种方法是使用liveData构建器函数,该函数返回一个LiveData,该LiveData在底层使用协程获取要通过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.
}
}

要以一种更类似于您的代码的方式做到这一点(只是为了帮助理解,因为这不是很简洁),您可以创建一个mutableelvedata并从您的协程发布到LiveData。

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.
}
}

如果您还没有准备好深入研究协程,您可以定义DAO来返回LiveData而不是挂起。它将开始从数据库中读取项目,并在准备好后通过实时数据发布它。

@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()
//...
}

返回值与预期一致,因为launch总是返回一个代表后台进程的Job对象。

我不知道你想如何使用String,但是在接收String之后应该完成的所有操作都必须移动到Coroutine内部或从Coroutine调用的函数中。

viewModelScope.launch {
val word = repository.readWord()

// do stuff with word

// switch to MainThread if needed
launch(Dispatchers.Main){}
}

最新更新