Search code examples
androidandroid-jetpack-composeandroid-roomandroid-livedataandroid-viewmodel

JetPack Compose + Room + LiveData + ViewModel


In a Jetpack Compose component I'm subscribing to Room LiveData object using observeAsState. The initial composition goes fine, data is received from ViewModel/LiveData/Room.

val settings by viewModel.settings.observeAsState(initial = AppSettings()) // Works fine the first time

A second composition is initiated, where settings - A non nullable variable is set to null, and the app crashed with an NPE.

DAO:

@Query("select * from settings order by id desc limit 1")
fun getSettings(): LiveData<AppSettings>

Repository:

fun getSettings(): LiveData<AppSettings> {
        return dao.getSettings()
}

ViewModel:


@HiltViewModel
class SomeViewModel @Inject constructor(
    private val repository: AppRepository
) : ViewModel() {

    val settings = repository.getSettings()
}

Compose:

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ItemsListScreen(viewModel: AppViewModel = hiltViewModel()) {
    val settings by viewModel.settings.observeAsState(initial = AppSettings())

Edit: Just to clearify, the DB data does not change. the first time settings is fetched within the composable, a valid instance is returned.

Then the component goes into recomposition, when ItemsListScreen is invoked for the second time, then settings is null (the variable in ItemsListScreen).


Solution

  • Once the LiveData<Appsettings> is subscribed to will have a default value of null. So you get the default value required by a State<T> object, when you call LiveData<T>::observeAsState, followed by the default LiveData<T> value, this being null

    LiveData<T> is a Java class that allows nullable objects. If your room database doesn't have AppSettings it will set it a null object on the LiveData<AppSettings> instance. As Room is also a Java library and not aware of kotlin language semantics.

    Simply put this is an interop issue.

    You should use LiveData<AppSettings?> in kotlin code and handle null objects, or use some sort of MediatorLiveData<T> that can filter null values for example some extensions functions like :

    @Composable
    fun <T> LiveData<T?>.observeAsNonNullState(initial : T & Any, default : T & Any) : State<T> =
        MediatorLiveData<T>().apply {
            addSource(this) { t -> value = t ?: default }
        }.observeAsState(initial = initial)
    
    @Composable
    fun <T> LiveData<T?>.observeAsNonNullState(initial : T & Any) : State<T> =
        MediatorLiveData<T>().apply {
            addSource(this) { t -> t?.run { value = this } }
        }.observeAsState(initial = initial)