Issue
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)
Answered By - Mark
Answer Checked By - David Goodson (JavaFixing Volunteer)