Kotlin Coroutines - 不能 return 来自房间数据库的对象
Kotlin Coroutines - cannot return object from room db
我不是很确定我在这里做什么,所以请放轻松:
我正在制作 wordle 克隆,要猜测的单词作为字符串存储在预先填充的房间数据库中,我正在尝试将其检索到我的 ViewModel,目前正在获取:
"StandaloneCoroutine{Active}@933049a"
而不是实际数据。
我试过使用只返回 null 的 LiveData,据我所知这是因为没有观察到它。
切换到协同程序,如果我的 UI 不需要数据,这似乎更有意义。
到目前为止我得到了这个:
道:
@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
)
虚拟机:
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
有更好的方法,我就是想不出来。
return 值符合预期,因为 launch 总是 return 代表后台进程的 Job
对象。
我不知道你想如何使用String
,但是所有应该在接收到String
之后进行的操作必须移动到Coroutine
里面或者在一个从 Coroutine
.
调用的函数
viewModelScope.launch {
val word = repository.readWord()
// do stuff with word
// switch to MainThread if needed
launch(Dispatchers.Main){}
}
您只能在启动块内访问 repository.readWord()
的 return 值。
viewModelScope.launch {
val word = repository.readWord()
Log.d("TAG", ": $word") // Here you will get the correct word
}
如果你需要在从数据库中获取这个词时更新你UI,你需要使用像LiveData
或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()
}
}
}
你可以在你的 UI 层收集这个 Flow,
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
变量中。
(现在我在电脑前,我可以多写一点。)
您当前代码存在的问题:
您无法在主线程上安全地同步读取数据库。这就是为什么 suspend
关键字会用在 DAO/repository 中的原因。这意味着,您无法在 ViewModel class 中拥有 non-nullable word
属性 并在 init
块中初始化。
协程是异步的。当您调用 launch
时,它正在排队让协程开始工作,但是 launch
函数 return 是一个作业,而不是协程的结果,您的代码在 launch
调用在同一个线程上继续。 launch
调用中的代码被发送到协程系统成为 运行 并且挂起调用在大多数情况下,就像在这种情况下一样,来回切换到后台线程。因此,当您在 Job 上调用 toString()
时,您只是获得协程 Job 本身的字符串表示,而不是其工作的结果。
由于协程是异步工作的,当您尝试在 launch
块下记录结果时,您在协程甚至有机会获取值之前就记录了它然而。因此,即使您已将协程的结果分配给某个 String 变量,在您记录它时它仍然为 null。
为了让你的数据库词在协程之外可用,你需要把它放在 LiveData 或 SharedFlow 之类的东西中,这样代码中的其他地方就可以订阅它,并在它到达时用它做一些事情。
SharedFlow 是一个非常大的学习主题,所以我将在下面的示例中使用 LiveData。
使用挂起函数创建 LiveData 来检索单词的一种方法是使用 liveData
构建器函数,return 是一个 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.
}
}
要以与您的代码更相似的方式执行此操作(只是为了帮助理解,因为这不太简洁),您可以创建一个 MutableLiveData 并从您的协程发布到 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 定义为 return 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()
//...
}
我不是很确定我在这里做什么,所以请放轻松:
我正在制作 wordle 克隆,要猜测的单词作为字符串存储在预先填充的房间数据库中,我正在尝试将其检索到我的 ViewModel,目前正在获取:
"StandaloneCoroutine{Active}@933049a"
而不是实际数据。
我试过使用只返回 null 的 LiveData,据我所知这是因为没有观察到它。
切换到协同程序,如果我的 UI 不需要数据,这似乎更有意义。 到目前为止我得到了这个:
道:
@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
)
虚拟机:
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
有更好的方法,我就是想不出来。
return 值符合预期,因为 launch 总是 return 代表后台进程的 Job
对象。
我不知道你想如何使用String
,但是所有应该在接收到String
之后进行的操作必须移动到Coroutine
里面或者在一个从 Coroutine
.
viewModelScope.launch {
val word = repository.readWord()
// do stuff with word
// switch to MainThread if needed
launch(Dispatchers.Main){}
}
您只能在启动块内访问 repository.readWord()
的 return 值。
viewModelScope.launch {
val word = repository.readWord()
Log.d("TAG", ": $word") // Here you will get the correct word
}
如果你需要在从数据库中获取这个词时更新你UI,你需要使用像LiveData
或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()
}
}
}
你可以在你的 UI 层收集这个 Flow,
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
变量中。
(现在我在电脑前,我可以多写一点。)
您当前代码存在的问题:
您无法在主线程上安全地同步读取数据库。这就是为什么
suspend
关键字会用在 DAO/repository 中的原因。这意味着,您无法在 ViewModel class 中拥有 non-nullableword
属性 并在init
块中初始化。协程是异步的。当您调用
launch
时,它正在排队让协程开始工作,但是launch
函数 return 是一个作业,而不是协程的结果,您的代码在launch
调用在同一个线程上继续。launch
调用中的代码被发送到协程系统成为 运行 并且挂起调用在大多数情况下,就像在这种情况下一样,来回切换到后台线程。因此,当您在 Job 上调用toString()
时,您只是获得协程 Job 本身的字符串表示,而不是其工作的结果。由于协程是异步工作的,当您尝试在
launch
块下记录结果时,您在协程甚至有机会获取值之前就记录了它然而。因此,即使您已将协程的结果分配给某个 String 变量,在您记录它时它仍然为 null。
为了让你的数据库词在协程之外可用,你需要把它放在 LiveData 或 SharedFlow 之类的东西中,这样代码中的其他地方就可以订阅它,并在它到达时用它做一些事情。
SharedFlow 是一个非常大的学习主题,所以我将在下面的示例中使用 LiveData。
使用挂起函数创建 LiveData 来检索单词的一种方法是使用 liveData
构建器函数,return 是一个 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.
}
}
要以与您的代码更相似的方式执行此操作(只是为了帮助理解,因为这不太简洁),您可以创建一个 MutableLiveData 并从您的协程发布到 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 定义为 return 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()
//...
}