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!