Home > database >  Kotlin Coroutines: NullPointerException on androidx.lifecycle.MutableLiveData object
Kotlin Coroutines: NullPointerException on androidx.lifecycle.MutableLiveData object

Time:10-31

I'm attempting to use kotlin Coroutines for fetching data from a Database using androidx.room artifact. I've analysed the code and I'm yet to find a solution to the problem.

I'm getting a null exception on tonight object which I already set to be nullable. I shouldn't be getting a null exception on a nullable object.

This is the ViewModel class from where I'm writing logic for fetching data

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

The Error Message

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$1.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)

CodePudding user response:

This is one of a couple of unexpected ways you can get a NullPointerException in Kotlin. You have an init block that calls the function initializeTonight() before tonight has been initialized. When a class is instantiated, all the property initializations and init blocks are called in order from top to bottom.

You might think it's safe because the value of tonight is set inside a coroutine, but by default viewModelScope synchronously starts running part of the launched coroutine because it uses Dispatchers.Main.immediate. Your getTonightFromDatabase() is calling a suspend function too, but that database might also be using Dispatchers.Main.immediate and be capable of returning a result without actually suspending.

I would change your code as follows. Remove the init block and initializeTonight functions. Declare tonight like this:

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

Also, I would make it val instead of var. There shouldn't be a reason to ever replace it, so making it var is error-prone.

CodePudding user response:

@Tenfour04 covered the issue that's causing it, but I just wanted to point out you're reading the error message wrong, and it probably sent you in the wrong direction:

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

That means you're trying to call setValue on a null object - i.e. null.setValue(whatever). It's not your MutableLiveData contents that are null (as you said, the value has a nullable type so that should be fine) - it's the MutableLiveData itself, which is what you call setValue on.

You don't see this one too often in Kotlin (since it does its own null checking and throws a different "oi this shouldn't be null" message) but it can happen!

  • Related