Kotlin 协程:androidx.lifecycle.MutableLiveData 对象出现 NullPointerException

Kotlin Coroutines: NullPointerException on androidx.lifecycle.MutableLiveData object

我正在尝试使用 kotlin 协程通过 androidx.room 工件从数据库中获取数据。我已经分析了代码,但我还没有找到解决问题的方法。

我在已设置为可为空的 tonight 对象上收到空异常。我不应该在可为 null 的对象上得到 null 异常。

这是 ViewModel class 我正在从中编写用于获取数据的逻辑

SleepTrackerViewModel.kt

package com.google.samples.apps.trackmysleepquality.sleeptracker

import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.viewModelScope
import com.google.samples.apps.trackmysleepquality.database.SleepDatabaseDao
import com.google.samples.apps.trackmysleepquality.database.SleepNight
import com.google.samples.apps.trackmysleepquality.formatNights
import kotlinx.coroutines.launch

/**
 * ViewModel for SleepTrackerFragment.
 */
class SleepTrackerViewModel(
    val database: SleepDatabaseDao,
    application: Application
) : AndroidViewModel(application) {

    init {
        initializeTonight()
    }

    /**
     * [tonight] is the object that holds the most recent [SleepNight]
     */
    private var tonight = MutableLiveData<SleepNight?>()

    /**
     * Get all the nights from the database
     */
    private val nights = database.getAllNights()
    val nightsString = Transformations.map(nights) { nights ->
        formatNights(nights, application.resources)
    }

    private fun initializeTonight() {
        viewModelScope.launch {
            tonight.value = getTonightFromDatabase()
        }
    }

    private suspend fun getTonightFromDatabase(): SleepNight? {
        var night = database.getTonight()
        if (night?.endTimeMilli != night?.startTimeMilli) {
            // If the start and end times are not the same, meaning that the night has already been completed
            night = null
        }
        return night
    }

    /**
     * Function to start tracking a new SleepNight
     */
    fun onStartTracking() {
        viewModelScope.launch {
            val newNight = SleepNight()
            insert(newNight)
            //assign newNight to tonight as the most recent SleepNight
            tonight.value = getTonightFromDatabase()
        }
    }

    private suspend fun insert(night: SleepNight) {
        database.insert(night)
    }

    fun onStopTracking() {
        viewModelScope.launch {
            val oldNight = tonight.value ?: return@launch
            oldNight.endTimeMilli = System.currentTimeMillis()
            update(oldNight)
        }
    }

    private suspend fun update(night: SleepNight) {
        database.update(night)
    }

    fun onClear() {
        viewModelScope.launch {
            clear()
            tonight.value = null
        }
    }

    suspend fun clear() {
        database.clear()
    }
}

错误信息

E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.google.samples.apps.trackmysleepquality, PID: 21352
    java.lang.NullPointerException: Attempt to invoke virtual method 'void 
androidx.lifecycle.MutableLiveData.setValue(java.lang.Object)' on a null 
object reference
        at 
com.google.samples.apps.trackmysleepquality.sleeptracker.SleepTrack
erViewModel$initializeTonight.invokeSuspend(SleepTrackerViewMod
el.kt:56)
        at 
kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(Contin
uationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
        at android.os.Handler.handleCallback(Handler.java:873)
        at android.os.Handler.dispatchMessage(Handler.java:99)

这是您在 Kotlin 中获得 NullPointerException 的几种意想不到的方法之一。在初始化 tonight 之前,您有一个 init 块调用函数 initializeTonight()。当一个class被实例化时,所有的属性初始化和init块被按从上到下的顺序调用。

你可能认为它是安全的,因为 tonight 的值是在协程内部设置的,但默认情况下 viewModelScope 同步启动启动协程的 运行 部分,因为它使用 Dispatchers.Main.immediate。您的 getTonightFromDatabase() 也在调用挂起函数,但该数据库也可能正在使用 Dispatchers.Main.immediate 并且能够在不实际挂起的情况下返回结果。

我会按如下方式更改您的代码。删除 init 块和 initializeTonight 函数。像这样声明 tonight

private var tonight = MutableLiveData<SleepNight?>().also {
    viewModelScope.launch {
        it.value = getTonightFromDatabase()
    }
}

另外,我会把它设为 val 而不是 var。不应该有替换它的理由,因此将其设置为 var 很容易出错。

@Tenfour04 涵盖了导致它的问题,但我只是想指出您错误地阅读了错误消息,它可能向您发送了错误的方向:

java.lang.NullPointerException: Attempt to invoke virtual method 'void 
androidx.lifecycle.MutableLiveData.setValue(java.lang.Object)' on a null 
object reference

这意味着您正在尝试在 null 对象上调用 setValue - 即 null.setValue(whatever)。这不是你的 MutableLiveData contents 是空的(正如你所说,该值具有可空类型,所以应该没问题) - 它是 MutableLiveData 本身,它就是你所说的 setValue on.

您在 Kotlin 中不会经常看到这种情况(因为它会进行自己的 null 检查并抛出不同的“oi this shouldn't be null”消息)但它可能会发生!