I have a simple insertion in ROOM but for a weird reason my insertion aren't working in my app after two or three insert. There is another weird behaviour, when inserting I have a success insert but when looking in the db, there is nothing.
DAO
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(entryTable: EntryTable): Completable
ViewModel
entryRepository.saveEntryTable(EntryTable(fieldId, entryId, tabId, index))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
Timber.d("<<<<<row saved!")
result.value = Status.SUCCESS
}, {
Timber.e(it)
Timber.d("<<<<<<<<<<<row not saved!")
result.value = Status.ERROR
})
The OnSubscribe is always triggered, but after two inserts, nothing is present in db and I have no errors in my log.
Also there aren't any replace, because each time there is a new PK.
Edit
Here more information about this issue:
The weird thing is it's look like the Room Database is in a way copy in memory, when my app is looking for new records, I can have them, like (select * from entry_table), and show them to the user. But if I stop the app and restart it, the row are gone (since they aren't in the db)..
I found an answer or I could say a solution to it, but for the moment I don't understand why this behaviour changed, because I didn't change anything in my app.
By changing the JournalMode from WRITE_AHEAD_LOGGING (or automatic) to TRUNCATE, all my changes are directly performed in the DB and I don't have any weird memory DB.
Related
No code, just a theoretical question.
The scenario is this
Room database with one table which houses the fields necessary.
Flow is setup to watch for database changes and report back to the UI.
So. sitting on page
/user/5
There is a delete button. Click the delete button the event passes up to the viewmodel and the viewmodel reacts.
UIDisplayUserDetails.kt
#composable
Button {
onclick:
event -> deleteClicked
navigation("userlist")
==================
viewmodel.kt
event deleteclicked {
repository.deleteUser(5)
}
=================
The design flaw is as follows.
The user deleted from the Room database works great. But that change in the Room database causes the Flow to recognize a change in the table in the Room database. That in turn sends the changes down stream. Which in turn causes a recompose.
The recompose causes a recompose on /user/5 which no longer exists in the database. Then things break.
What would be the correct method to delete a record from the Room database without causing a recompose. or the correct way to think about how to do this.
thanks
Tried to delete a record from the Room database, which works, but that causes a recompose. I either don't want the recompose, or want a new way to think about this design pattern
If you set things up like below then it should recompose UsersRow() from the state of users changing which would then leave out user 5 entirely when it is deleted from Room. This would avoid a crash I beleve.
#Composable
fun UsersRow(context: Context, viewModel: UserViewModel) {
val users by viewModel.getUsers().observeAsState() // Observing livedata/flow
Row {
users.forEach { user ->
UserCard(user)
}
}
}
List docs
State and Jetpack Compose
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'm using this library for wrapping Firebase transactions with RxJava. I'm new to RxJava, so this is mainly a question regarding how to use it.
Scenario: There is a many-to-many relationship between Persons and Labels. A Person can have multiple Labels, and a Label can be given to many Persons. When a Person is created, I must:
add them to the list of Persons
update each Label given to them to allow for querying all Persons that belong to a specific label
I have a list of Labels I want to write to my Firebase database.
List<Label> labels; // Let's assume it's been instantiated and added to
I want to write each of these to the DB:
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference peopleRef = database.getReference().child("people");
DatabaseReference labelsRef = database.getReference().child("labels");
int newPersonId = peopleRef.push().getKey();
I can do this easily if I don't care about whether the calls are successful.
// Let's assume I already saved the Person to the DB
for (Label label : labels){
// For each label, index the Person saved (Looks like 'personId: true')
labelsRef.child(label).child(newPersonId).setValue(true);
}
But what if I do care about the result? If I want to react to all Labels being updated (like navigate away from the current Activity), I need to know if they've all been updated successfully.
RxFirebase is implemented such that setting a value in the DB returns a Completable. I essentially want to zip together n number of Completables and do something only when they succeed or fail.
So far, I can do this if I only want to update one Label, but I want to update n Labels.
The following code snippet chains 2 Completables together, but only saves 1 Label
RxFirebaseDatabase.setValue(peopleRef.child(newPersonId), person) // Save the Person
.andThen(RxFirebaseDatabase.setValue(labelsRef.child(label).child(newPersonId), true)) // I can index 1 Label, and this returns a Completable
How would I do this? If you know Firebase well enough, is this even the right way to be saving a List of items?
If I understood your main question correctly, you have a collection of Completable and you need to subscribe to them as one.
The way to solve this is using the Completable.concat or Completable.merge operators.
Completable.concat: Returns a Completable which completes only when all sources complete, one after another.
Completable.merge: Returns a Completable instance that subscribes to all sources at once and completes only when all source Completables complete or one of them emits an error.
Example:
List<Completable> tasks; // initialized elsewhere
Completable
.concat(tasks)
.subscribe(
() -> Log.d(TAG, "All successful"),
throwable -> Log.w(TAG, "One or more failed"))
About your second question, I don't know Firebase well enough.
Update: to obtain the List<Completable> you can do something similar to this:
List<Completable> tasks = new ArrayList<>();
for ( ... ) {
tasks.add(RxFirebaseDatabase.setValue(peopleRef.child(newPersonId), person));
}
Completable.concat(tasks).etc
Fetching data from DB , Room DAO has a method that returns a Flowable userDao.getInfo(), this Flowable will never completes, I tested adding doOnNext() it emits 5 times (DB contains 5 items) but complete is never called, but I need as I have toList(),what could be the alternative for this
return userDatas()
.flatMapIterable(items -> items)
.flatMap(userData -> userDao.getInfo(userData.getId())
.map(user -> user.toStoreModel(...)//added doOnNext()-works 5 times and doOnComplete()doesn't work
.doOnNext(userData -> Log.i("test",""+userData))
.doOnComplete(() -> Log.i("test","complete"))
.toList()
.map(UserModel::fromUserModels)
.toFlowable();
#Query("SELECT * FROM user WHERE id = :id")
Flowable<...> getInfo(Long Id);
public Flowable<List<UserStore>> userDatas() {
return userDao.allUserDatas()
.take(1)//added complete and next works
.filter(userDatas -> !userDatas.isEmpty())
.switchIfEmpty(userIds()
.doOnNext(userDatas -> userDao.insert(userDatas)));
}
I have tested and even when I'm replacing userDatas() only with userDao.allUserDatas() (I'm sure it exists in DB) it gives the same results
If you need to have called complete method you can use take(1).but in that case you could not listener further DB changes
Everything is ok with your code ,it would never complete
Db Flowables are observable ,so they keep listening if database changes, so it never completes.
Ideally you should fix userDao so that it completes normally. If that is not possible for some reason, you can time it out and map error to empty, forcing completion like so:
userDao.getInfo(userData.getId())
.timeout(1, TimeUnit.SECOND)
.onErrorResumeNext(Observable.empty())
I have Android Room's SQL query, that return flowable:
#Query("SELECT * FROM exercices WHERE lang = 'ru' AND id_exercice = :id")
Flowable<Exercices> getExercicesById(int id);
In my repository, I need get emitted element, than change his boolean value, then call new method, that must returns completable.
That's what I try:
#Override
public Completable setExerciseUsed(int id) {
return mDatabase.exerciseDao().getExercicesById(id)
.doOnNext(exercise -> exercise.setIs_used(1))
.flatMapCompletable(exercise ->
Completable.fromAction(() -> mFitnessDatabase.exerciseDao().addExercise(exercise)));
}
Also i tried:
Exercices e = mDatabase.exerciseDao().getExercicesById(id).blockingFirst();
e.setIs_used(0);
return Completable.fromAction(() -> mDatabase.exerciseDao().addExercise(e));
But t not works properly. It seems like flowable emits many elements, and it going to stuck in cycle, after subscription.
Since your DAO returns Flowable, it will emit fresh data each time table is modified.
So after calling mFitnessDatabase.exerciseDao().addExercise(exercise),
getExercicesById will emit new data, thus the chain will execute forever.
If you want Room not to emit data - just change Flowable to Single.
Since you expect the one value to e returned, it's a good idea to limit the result to one item: "SELECT * FROM exercices WHERE lang = 'ru' AND id_exercice = :id LIMIT 1".
But actually, in your case, if you want to change a parameter of an item, it's much more efficient to do this within one query.
It may look like:
#Query("UPDATE exercices SET is_used = 1 WHERE lang = 'ru' AND id_exercice = :id")
fun setIsUsed(id: Int)
I don't know much about flowables or how to unsubscribe from one source with those, but have you thought about using LiveData? You can simply add a source to a LiveData object, then remove the connection to the source, change the object and execute your method without going into loop.
Another idea would it be that you retrieve your object, keep the connection and updating the value outside of that. Because you are keeping the database connection with LiveData, the observer would execute the onChanged again (because the source = database has been changed). You only would need to make sure that this method returns for example null (via setValue) for the time being until the database uploaded the new object value.
If you want to stay with flowables, maybe you should consider adding a condition which will prevent the app from re-applying the integer to the object (and sending it to the database). Does that makes sense to you?