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