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,你需要使用像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 层收集这个 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()

    //...
}