Note: Room checks invalidations based on table modifications, which means it may dispatch false positive notifications. Guide to app architecture
What is an invalidation for Room? How does it cause false positive notifications?
That means,
Suppose you have below query
#Query(“SELECT * FROM Users WHERE userId = :id)
fun getUser(id: String): LiveData<User>
And you are observing it like
getUser("id_1").observe(this, Observer{
// do something
})
There is nothing wrong in above method, but there is the case of false positive notifications.
Now suppose that from somewhere else you have deleted user with an userId = "id_2". At this point, you know that you don't need to get notified for your earlier getUser("id_1") call as it has nothing to do with your operations on id_2. But you still will get notified and your // do something will run again. That's because, Room will know that something has been changed, but doesn't know what has been changed and hence it will just re-query and send the result back again.
In order to bypass this false positive notification, you can use MediatorLiveData when you have LiveData as return type or distinctUntilChanged if you are have RxJava as return type in your Daos.
Ref: 7 Pro-tips for Room
Related
In room, I have a dao to something like this:
#Dao
interface FacultyDao {
#Query("select * from faculty")
fun getAll(): LiveData<List<Faculty>>
...
}
And inside the repository, I'm simply calling this method and logging it:
class FacultyRepository(application: Application) {
private val facultyDao: FacultyDao
init {
val db: AppDB = AppDB.getInstance(application)
facultyDao = db.facultyDao()
}
fun getAllFaculty(): LiveData<List<Faculty>> {
val v = facultyDao.getAll()
Log.d("muaxx", v.value.toString())
return v
}
...
}
But the thing is it's returning me null, but when I ran that query in inspector it worked. Am I missing something?
LiveData doesn’t immediately have an initial value. Room queries the database and gets the result on a background thread. Then on the next loop of the main thread, the LiveData’s value will be set to this retrieved value. You are logging value too early. The initial value is going to appear some time in the future, after this function has already returned.
Normally you should only be getting a LiveData value through observing it.
Directly checking the value should usually only be done when you are managing a MutableLiveData and are using the previous value to help determine the next value that you are going to post.
Live data gives us real-time data. Therefore, for the first time, you still don't have some in yourself. And it is waiting for the response of the database. If you want to see some of the live data, you must observe it so that after receiving the information, the observer will be called and the information will be logged.
Can I create a StateFlow from a Flow so that I can get the .value of it? Is there a way to do it without using .collect?
Something like
val myStateFlow = StateFlow<MyDataType>(this.existingFlow)
So that later, when someone clicks a button, I can say what the last value of the flow is?
fun handleButton() {
val lastValue = myStateFlow.value
print(lastValue)
}
I would prefer not using collect, since I dont want the flow to flow until someone else decides to collect it.
There is a built-in function called stateIn.
Is there a way to do it without using .collect?
I would prefer not using collect, since I dont want the flow to flow until someone else decides to collect it.
If you use the non-suspending overload of stateIn, the flow collection doesn't have to start right away, but you have to provide an initial value for the state (because this call cannot wait for the first value of the flow if it doesn't suspend).
In that case, you can play with the started argument to make the actual collection lazy, but note that accessing myStateFlow.value won't trigger the collection of the flow and will just return the initial value over and over. Only terminal flow operators (like collect) on the StateFlow will actually trigger the underlying flow collection, which is probably not what you want (but maybe it is!).
Note that you have to have a coroutine running to get the values from the initial flow and set the state accordingly if you want to access values via myStateFlow.value. This is actually what stateIn does by default: it starts a coroutine that collects the flow to set the StateFlow's value - so it technically uses collect.
I've got a question about Flowables. I already have a few solutions for this issue, but I would like to double-check if these are the best possible solutions or not.
Context
I have an Interactor that is supposed to bookmark recipes on the DB. It looks like this:
/**
* This Interactor marks a recipe as "bookmarked" on the DB. The Interactor actually switches
* the isBookmarked value of the related recipeId. If it was marked as true, it switches its value
* to false. If it was false, then it switches its value to true.
*/
class BookmarkRecipeInteractorImpl(
private val recipesCacheRepository: RecipesCacheRepository
) : BookmarkRecipeInteractor {
override fun execute(recipeId: Int, callback: BookmarkRecipeInteractor.Callback) {
// Fetches the recipe from DB. The getRecipeById(recipeId) function returns a Flowable.
// Internally, within the RecipesCacheRepository, I'm using room.
recipesCacheRepository.getRecipeById(recipeId).flatMap { originalRecipe ->
// Switches the isBookmarked value
val updatedRecipe = originalRecipe.copy(
isBookmarked = !originalRecipe.isBookmarked
)
// Update the DB
recipesCacheRepository.updateRecipe(updatedRecipe)
// Here's the issue, since I'm updating a DB record and the getRecipeById returns
// a Flowable, as soon as I update the DB, the getRecipeById is going to get triggered
// again, and switch the value again, and again, and again...
}
.subscribe(
{
callback.onSuccessfullyBookmarkedRecipe(it.response)
},
{
callback.onErrorFetchingRecipes()
}
)
}
}
So, if you follow the code, the error is pretty straightforward. I get stuck on a loop, where I constantly change the recipe record.
Possible solutions
1) Have two different functions on my DAO, one called getRecipeByIdFlowable(id) that returns a Flowable, and another called getRecipeByIdSingle(id) that returns a rx.Single. That way I can expose the getRecipeByIdSingle(id) through the Repository and use it instead of the function that returns the Flowable. That way I cut the loop.
Pro: It works.
Con: I don't like having functions like this on my DAO.
2) Save the Disposable on a lateinit property and dispose it as soon as the subscriber triggers the onNext().
Pro: It works.
Con: I don't like having to do something like this, feels hacky.
3) Using ...getRecipeById(recipeId).take(1).flatMap... so it only handles the first emitted object.
Pro: It works, it looks tidy.
Con: I'm not sure if there's a better way to do it.
Question
Ideally, I would like to call some function that just allows me to disable the Flowable behavior and prevent it from emitting more items if the DB changes. So far the solution that I like the most is #3, but I'm not really sure if this is the right way to do it.
Thanks!
Edit 1
I'm just adding a bit more of information about the use case here. I need an Interactor that given a recipeId changes the isBookmarked value on DB to its oposite.
The DB records look like:
data class DbRecipeDto(
#PrimaryKey
val id: Int,
val name: String,
val ingredients: List<String>,
val isBookmarked: Boolean = false
)
I know that maybe there's some other ways in which I could tackle this issue differently. Maybe I could pass the recipeId arg and a bookmark (Boolean) argument and just run the update query.
But this use case it is totally made up, just an example; The thing that I'm trying to figure out how to prevent a Flowable from emitting more items if something changes on the DB.
You should probably call .take(1).singleOrError() on the end of getRecipeById(recipeId).
This will take the first item (or the error) emitted by the Flowable retrieved by calling getRecipeById and wrap it in a Single. In my opinion this correctly matches the semantics of what you want to achieve.
In addition, if I recall correctly, because you will be subscribing on a Single by doing this, your Flowable will not continue to do work after the first item is consumed by the downstream call to singleOrError.
I have a case where I have LiveData observer that monitors a condition that indicates if the user is signed in. The observer will only get notified when the user is signed in. I don't need to pass any data to the observer. When the observer gets called, it simply means that the user is signed in:
val observer = Observer<String> { signedIn ->
// The user is signed in. Do something...
}
model.isSignedIn.observe(this, observer)
In my viewmodel I believe I'm suppose to update the observer as follows:
isSignedIn.setValue()
Is this the proper way to update an observer that doesn't require any data sent to it? LiveData is really about notifying observers about data changes. But in my example, I'm using it to notify about an event change. It's a subtle difference and maybe using LiveData for this case is not the best way of doing it.
In that case you can use LiveData, it has no restrictions, especially if you want to be lifecycle aware.
If you want to have more clear API for that case you can use extension function mechanism. And in your case, suggest to use Unit type for live data variable.
typealias NoValueLiveData = MutableLiveData<Unit>
fun NoValueLiveData.setValue() {
this.value = Unit
}
I'm using Room and in the Dao I have this method:
LiveData<List<Books>> getAllBooks();
In MainActivity I have subscribed to that method from the ViewModel. Changes to the data trigger the onChanged() callback:
viewModel.getAllBooks()
.observe(this, books -> {
Log.d(TAG, "onChanged()");
booksListAdapter.setData(new ArrayList<>(books));
});
What I would like to know is what constitutes an update? When the app first starts I do 100 insertions, each of them changes the database but the onChanged() is not invoked 100 times. Last time I checked it called onChanged() the first time which I think it always calls when starting and then two more calls.
Can I have control over this? For example if I know I will be doing 100 insertions perhaps it would be better if I only got the callback at the end of the insertions.
You don't have control of that. What you can do is use MediatorLiveData and post the value after all insertions. Whenever you update, delete or insert Room knows that there has been change but doesn't know what has been changed. So it just re-queries and sends the results to observing LiveData
Check this blog and mainly section 7. Avoid false positive notifications for observable queries. Author gives pretty good example of MediatorLiveData which is similar to what you are looking for