I'm studying Rxjava2 and I'm trying to integrate the Room Library with Rxjava2. The problem is: I have a populated table and every time I login in the app, I need to delete this table and then insert a new content in database. Separately, the delete and insert works fine, but when I try to insert new values after I delete the table content, the delete method deletes all the new values.. (some parts of the code is in kotlin and others in java)
I already tried this: RxJava2 + Room: data is not being inserted in DB after clearAllTables() call, but no success..
DAO
#Dao
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(list:List<Something>)
#Query("DELETE FROM SomethingTable")
fun delete()
#Query("SELECT * FROM SomethingTable")
fun getAll(): Flowable<List<Something>>
My class that calls the DAO (CallDao)
//insert
fun insertInDB(list: List<Something>) {
Completable.fromAction {
dbDAO!!.insert(list)
}.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe()
}
//delete
fun clean() {
Completable.fromAction {
dbDAO!!.delete()
}.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.single())
.subscribe()
}
//search all
fun findAll(): Observable<List<Something>>? {
return Observable.create { subscriber ->
dbDAO!!.getAll()
.subscribeOn(Schedulers.io())
.subscribe {it->
subscriber.onNext(it)
}
}
}
Method that is called when I click in login button
private void clearAndInsertInDB() {
CallDao callDao= new CallDao(getActivity());
//delete all table values
callDao.clean();
Something sm = new Something("test1", "test2");
ArrayList<Something> list = new ArrayList<>();
list.add(sm);
list.add(sm);
//insert new values
callDao.insertInDB(list);
//get all new values in DB
callDao.findAll()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(res -> {
//here gives me a IndexOutOfBoundsException
Log.d("logDebug", res.get(0).getCodeExemple());
});
}
Any corrections in my code is also welcome :) , but the main problem is that the delete method deletes all the new insert values and it should delete only the old values.
You are making two asynchronous calls: one to delete the users and another to insert them again. However, even though you call first the callDao.clean(); method and after that you call callDao.insertInDB(list); , it is not guaranteed that the clean() operation will finish before the insertInDB() operation (because that's how asynchronous calls work).
This is what is happening:
Instead, you should chain your async calls , in such a way that the second one gets called as soon as you know that the first one has already finished.
How to achieve that using RxJava and Completable? Using the andThen operator as stated in this answer
You should modify your clean() and insertInDB() methods to return the Completables, use andThen to chain them, and then subscribe.
Simple example using RxJava and andThen()
FakeDatabase db = Room.databaseBuilder(this, FakeDatabase.class, "fake.db")
.fallbackToDestructiveMigration()
.build();
UserDao userDao = db.userDao();
User user1 = new User("Diego", "Garcia Lozano", "diegogarcialozano#fake.com");
User user2 = new User("Juan", "Perez", "juanperez#fake.com");
User user3 = new User("Pedro", "Lopez", "pedrolopez#fake.com");
List<User> users = new ArrayList<>();
users.add(user1);
users.add(user2);
users.add(user3);
Completable deleteAllCompletable = Completable.fromAction(userDao::deleteAll);
Completable insertUserCompletable = Completable.fromAction(() -> userDao.insertAll(users));
deleteAllCompletable
.andThen(Completable.fromAction(() -> System.out.println("Delete finished")))
.andThen(insertUserCompletable)
.andThen(Completable.fromAction(() -> System.out.println("Insert finished")))
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.single())
.subscribe();
Checking the Logcat after execution, you can see that the operations were executed in the proper order:
2018-11-19 16:07:02.056 10029-10047/? I/System.out: Delete finished
2018-11-19 16:07:02.060 10029-10047/? I/System.out: Insert finished
Afterwards, I checked the content of the database using the tool SQLite Browser and saw that the insert worked properly.
Using #Transaction in the DAO
You can get a better solution for your problem without using RxJava at all. Instead, you can define a Transaction in your DAO using the #Transaction annotation, as explained in this post. It would look something like this:
Dao
#Dao
public abstract class UserDao {
#Transaction
public void deleteAndCreate(List<User> users) {
deleteAll();
insertAll(users);
}
#Query("DELETE FROM User")
public abstract void deleteAll();
#Insert
public abstract void insertAll(List<User> users);
}
Activity
Completable.fromAction(() -> userDao.deleteAndCreate(users))
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.single())
.subscribe();
Checking the table
Personally, I would do it with the #Transaction annotation.
Related
I'm using RoomDao with kotlin coroutines and Flow. What I'm trying to do is collect one Training with all its Exercises with all Repetitions per Exercise. Exercises and Repetitions are Flows, cuz this values can be changed and I want to observe them.
The problem is that when I updating exercises, getTrainingExerciseLinksBy doesn't triggers, and I don't know, why. Here is my code in UseCase:
suspend fun getTrainingWithExercisesAndRepetitionsBy(trainingId: Long): Flow<UiTrainingWithExercisesAndRepetitions> {
/// This method returns Flow<List<TrainingExerciseLink>>
return trainingExerciseLinksRepository.getTrainingExerciseLinksBy(trainingId).flatMapConcat { trainingExerciseLinks ->
trainingExerciseLinks.map { trainingExerciseLink ->
/// This method returns Flow<List<ExerciseRepetition>>
repetitionsRepository.getExerciseRepetitionsBy(trainingExerciseLink.id).map { repetitions ->
/// do some other selects for collecting data about exercise in one training
}.flowOn(Dispatchers.IO)
}.zipFlows()
}.flowOn(Dispatchers.IO)
}
In my ViewModel I'm observing this method like this:
viewModelScope.launch {
useCase.getTrainingWithExercisesAndRepetitionsBy(trainingId)
.distinctUntilChanged()
.collect {
_exercisesListLiveData.value = it.exercises
_trainingListLiveData.value = it.trainingData
}
}
What is wrong with this code?
UPD:
In my DAO I'm using Flows for subscribing on database's updates, like this:
#Dao
abstract class TrainingExerciseLinkDao {
#Query("select * from TrainingExerciseLink where trainingId = :trainingId")
abstract fun getTrainingExerciseLinksBy(trainingId: Long): Flow<List<TrainingExerciseLink>>
}
and ExerciseRepetitionsDao:
#Dao
abstract class ExerciseRepetitionDao {
#Query("select * from ExerciseRepetitionEntity where trainingExerciseId = :trainingExerciseId")
abstract fun getExerciseRepetitionsBy(trainingExerciseId: Long): Flow<List<ExerciseRepetitionEntity>>
}
Actually I found the answer, so maybe somebody will jump in the same gap and this thread will be helpful.
The problem in my code was that I used flatMapConcat. This operator waits emits from original Flow and from flatMapped Flow at one time, so in this case it will trigger callback. To fix this, flatMapLatest should be used. You can read more about difference between this operators here.
So my code now looks like this:
suspend fun getTrainingWithExercisesAndRepetitionsBy(trainingId: Long): Flow<UiTrainingWithExercisesAndRepetitions> {
/// This method returns Flow<List<TrainingExerciseLink>>
/// Here is main change: flatMapConcat -> flatMapLatest
return trainingExerciseLinksRepository.getTrainingExerciseLinksBy(trainingId).flatMapLatest { trainingExerciseLinks ->
trainingExerciseLinks.map { trainingExerciseLink ->
/// This method returns Flow<List<ExerciseRepetition>>
repetitionsRepository.getExerciseRepetitionsBy(trainingExerciseLink.id).map { repetitions ->
/// do some other selects for collecting data about exercise in one training
}.flowOn(Dispatchers.IO)
}.zipFlows()
}.flowOn(Dispatchers.IO)
}
You are using it wrong , as when database updates your getTrainingWithExercisesAndRepetitionsBy does not know,
to get over this issue use flows in your dao like this example as Room supports Flow then
viewModelScope.launch {
viewModel.yourFunctionThatGetsDataFromRepository(trainingId)
.distinctUntilChanged()
.collect {
_exercisesListLiveData.value = it.exercises
_trainingListLiveData.value = it.trainingData
}
}
and if more you can refer this example
I am using LiveData to observe a Room query that filters a table on a boolean in the table. The boolean is whether the row has been uploaded to an external database or not.
Here is the query I'm observing:
memodao.kt
#Query("SELECT * FROM memos WHERE uploaded = 0 ORDER BY id ASC")
fun getUnsyncedMemos(): LiveData<List<Memo>>
//including this statement because it's used later
#Update
suspend fun update(memo: Memo): Int
When the LiveData fires, I grab that row, upload it to the external database using Retrofit, and then when Retrofit returns I mark that row as uploaded to exclude it from the above query.
Writing to the table I am observing causes the above query to trigger again, as expected.
The trouble is, from time to time the query returns the same row again, even though it NO LONGER meets the where clause.
It actually returns a stale version of the row, that still meets the where clause.
It only happens from time to time. To me it feels like a race condition: the beginning of the Room #Update statement triggers the #Query to execute again, and sometimes the row hasn't been rewritten in time, so the #Query returns a stale result.
Here is the rest of the relevant code.
CloudSyncService.kt
mMemoRepository.getUnsyncedMemos().observeForever{ memoList ->
memoList.forEach { memo ->
if (!memo.uploaded) uploadMemo(memo) //an unnecessary if, I know.
}
}
private fun uploadMemo(memo: Memo) {
mMemoRepository.uploadMemo(memo).observeOnce(this, Observer {
mCloudOK = it != CLOUD_FAILED
})
}
MemoRepository.kt
override fun getUnsyncedMemos() = memoDao.getUnsyncedMemos()
override fun uploadMemo(memo: Memo): LiveData<Int> {
val mutableLiveData = MutableLiveData<Int>()
CoroutineScope(IO).launch{
memoWebService.uploadMemo(memo).enqueue(object : Callback<Memo> {
override fun onResponse(call: Call<Memo>, response: Response<Memo>) {
if (response.isSuccessful) {
memo.uploaded = true
updateMemo(memo)
mutableLiveData.value = CLOUD_OK
}
else {
mutableLiveData.value = CLOUD_UNKNOWN
}
}
override fun onFailure(call: Call<Memo>, t: Throwable) {
mutableLiveData.value = CLOUD_FAILED
mMutableErrorMessage.value = t.message
}
})
}
return mutableLiveData
}
I'm wondering if it's an issue with the co-routines throwing things out of sync and creating a race? I've moved calls in and out of co-routines to try to fix it, but I still get the problem.
I have read about the issue where LiveData returns stale data: Room LiveData fires twice with one stale emission
I believe my question differs, because I am not adding new observers that receive stale data in the first firing. I have one observer, observing forever, that is occasionally getting stale data after a db #Update.
Many thanks!
I want a way to search and update an existing entry in Room with RxJava. If there's no record it should create a new one.
For example, lets say I have the following queries:
#Insert
Single<Long> createContent(Content content);
#Query("SELECT * FROM Content WHERE contentId = :contentId")
Single<Content> searchContent(String contentId);
My Goals:
Check if there's a previous data and return its value
If there's no record create a new one and return it's value
Problem with this approach:
Whenever there's no record the from #Query, the Single<Content> directly goes to error ignoring any map/flatMap operator
The #Insert query returns a Single<Long> but the #Query returns a Single<Content>
Is there any way to call and return a new Observable from the error? Something like this:
daoAccess.searchContent(contentId)
.subscribeOn(Schedulers.io())
.map(Resource::success)
.onErrorResumeNext(new Function<Throwable, Single<Content>>() {
#Override
public Single<Content> apply(Throwable throwable) throws Exception {
return daoAccess.createContent(contentId);
}
})
You could use Single.onErrorResumeNext():
daoAccess.searchContent(contentId)
.subscribeOn(Schedulers.io())
.map(Resource::success)
.onErrorResumeNext(throwable ->
daoAccess.createContent(content)
.map(id -> Resource.success(content))
)
I am facing an issue while inserting data to Android table. Here is my Dao functions:
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(freight: Foo)
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(freights: MutableList<Foo>)
Here is how it is invoke:
Observable.fromCallable {
db.fooDao().insert(it)
}
}
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.subscribe {
Logger.d("Inserted ${it} users from API in DB...")
}
Exception I am getting:
Caused by: java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.next(ArrayList.java:860)
at com.blockgrain.blockgrain.dbmanager.repository.FooRepository$insertDataInDb$1.call(FooRepository.kt:76)
I have created other tables with same logic they are working fine but this one is failing . Please let me know what went wrong.
Update :
Foo.kt
override fun get(): Observable<MutableList<Foo>> {
val observable = getDataFromDb()
return observable.flatMap {
if (it.isEmpty())
getDataFromApi()
else
observable
}
}
override fun getDataFromApi(): Observable<MutableList<Foo>> {
return WebService.createWithAuth().getFooFromWeb()
.doOnNext {
Logger.d(" Dispatching ${it} users from API...")
Observable.fromCallable {
db.fooDao().insert(it)
}
}
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.subscribe {
Logger.d("Inserted ${it} users from API in DB...")
}
}
}
As per the given code, It is not directly clear how the array list modification is being called resulting into Caused by: java.util.ConcurrentModificationException .
My guess is, multiple operations are being performed on same list at a time.
Your insert list method in dao is accepting MutableList<Foo> change it to List<Foo> as Room doesn't need mutable list. like this,
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(freights: List<Foo>)
I would recommend to copy the array list to another list before doing any operations on the list like this
// Before performing any operation on list
var newList:List<Foo> = ArrayList<Foo>(otherList)
// Perform operation on newList - for ex.
db.insert(newList)
There is another solution for ArrayList if you want to use it concurrently with CopyOnWriteArrayList. But this will result into significant modification in existing in code. So I would recommend to go with first option.
I need to check if database is empty and if it is, then download data with retrofit2, select what I need and insert it to database and finally return inserted data from database. I have tried to do it with this example https://stackoverflow.com/a/48478847/5184417, but I can't figure out the part with inserting data to database. It gives an error about return of flatmap that insert to database doesn't return anything.
I have this piece of code
fun getEmployees(): Flowable<List<Employee>> {
return employeeDao.getEmployeeCount()
.take(1)
.flatMap { counts ->
if (counts.isEmpty() || counts[0] == 0) {
Api.getAPIService().getDepartments()
.flatMap{ response ->
employeeDao.deleteAll()
for (departments in response.Departments) {
if (departments.Name == "AR") {
for (employee in departments.employees) {
employeeDao.insert(employee)
}
}
}
state.postValue(RepositoryState.READY)
}
.ignoreElements()
.andThen(employeeDao.getAll())
}
employeeDao.getAll()
}
}
interface ApiService {
#GET("departments")
fun getDepartments() : Single<Departments>
}
#Dao
interface EmployeeDao {
#Query("SELECT * FROM employees")
fun getAll(): Flowable<List<Employee>>
#Query("SELECT count(1) FROM employees")
fun getEmployeeCount(): Flowable<List<Int>>
#Insert(onConflict = REPLACE)
fun insert(employee: Employee)
}
Thanks for any help!
flatmap is used to chain the observable. It's syntax is:
observable1
.flatmap(i-> {
return observable2;}
)
So the point is that you should return an Observable inside flatmap and that Observable will be propagated down(I mean observable 2 in above code). One possible solution is that make employeeDao.getAll() to return a flowable OR some how just wrap the output of employeeDao.getAll() inside Observable.just() or Observable.create() or whatever method you know.
EDIT: You must return an observable inside flatmap, you are not using any return statement.