Search code examples
javaandroidandroid-roomandroid-livedatamediatorlivedata

Room Database doesn't run my implemented transaction on background thread


This question is a solution I've tried for my other question, so if you have a solution to the first one that avoids this question problems, I'm all ears.

So I have this method in Dao (getPageCombinedData) that is annotated with @transaction and should return a MediatorLiveData, I thought since MediatorLiveData is a subclass of LiveData, then Room will provide the necessary code to return it using background thread as quoted from its documentation.

When Room queries return LiveData, the queries are automatically run asynchronously on a background thread.

but instead I got the exception telling me that I'm running my query on Main Thread

java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.

I firstly tried making this with an Executer on another thread but I found out that whenever the database gets updated, it will never reach my observer with the new data as my code looked like this

my ViewModel code

public LiveData<PagesCombinedData> getPageCombinedData(){
    MediatorLiveData<PagesCombinedData>dataLiveObj = new MediatorLiveData<>();
    executor.execute(()->{
                UserPageRelation userRelation = scheduleDB.userDao().getAllUserPages().getValue();
                dataLiveObj .postValue(scheduleDB.userDao().getPagesCombinedData().getValue());
            });
return dataLiveObj;
}

where that way will only update the returned LiveData only once when the thread finish executing and whenever the database gets updated, my observer in UI Fragment will never know about it.

so now I don't know how should I return this Mediator in order for my UI to observe it and update the UI whenever data is updated in the database.

My Fragment's code that should observe the Mediator

pagesViewModel.getPageCombinedData().observe(getViewLifecycleOwner(), pagesCombinedData -> {
            pagesAdapter.setUserPages(pagesCombinedData.getUserEntities());
            pagesAdapter.setPageEntities(pagesCombinedData.getPageEntities());
            pagesAdapter.notifyDataSetChanged();
});

My PagesViewModel Code :

public LiveData<PagesCombinedData> getPageCombinedData(){
        return scheduleDB.userDao().getPagesCombinedData();
}

my UserDao code that should construct the Mediator Object :

@Dao
public abstract class UserDao {
    @Transaction
    public MediatorLiveData<PagesCombinedData> getPagesCombinedData (){
        MediatorLiveData <PagesCombinedData> dataMediator  = new MediatorLiveData<>();
        LiveData<List<PageEntity>> value1 = getAllPages();
        LiveData<UserPageRelation> value2 = getAllUserPages();
        dataMediator.addSource(value1, value -> dataMediator.setValue(combineData(value1,value2)));
        dataMediator.addSource(value2, value -> dataMediator.setValue(combineData(value1,value2)));
        return dataMediator;
    }
    private PagesCombinedData combineData(LiveData<List<PageEntity>> pages,LiveData<UserPageRelation> userPages ){
        return new PagesCombinedData(pages.getValue()==null?new ArrayList<>():pages.getValue()
                ,userPages.getValue()==null ?new ArrayList<>():userPages.getValue().getUserPages());
    }
    @Query("Select * from page")
    public abstract LiveData<List<PageEntity>> getAllPages ();
    @Transaction
    @Query("Select * from user limit 1")
    public abstract LiveData<UserPageRelation> getAllUserPages ();   
}

Update

tried to move my code into the ViewModel but it tells me that I can't addsource on a background thread because it uses observeForever -_-*

the code I tried in my ViewModel

private MediatorLiveData<PagesCombinedData> pagesCombinedData;
private ExecutorService executor ;
public LiveData<PagesCombinedData> getPageCombinedData(){
        if (pagesCombinedData==null){
            pagesCombinedData = new MediatorLiveData<>();
            pagesCombinedData.setValue(new PagesCombinedData());
        }
        executor.execute(()->{
            LiveData<List<PageEntity>> value1 = scheduleDB.userDao().getAllPages();
            LiveData<UserPageRelation> value2 = scheduleDB.userDao().getAllUserPages();
            pagesCombinedData.addSource(value1, value -> pagesCombinedData.setValue(combineData(value1,value2)));
            pagesCombinedData.addSource(value2, value -> pagesCombinedData.setValue(combineData(value1,value2)));
        });
        return pagesCombinedData;
    }

the exception I got (line 59 is addSource line)

java.lang.IllegalStateException: Cannot invoke observeForever on a background thread
        at androidx.lifecycle.LiveData.assertMainThread(LiveData.java:487)
        at androidx.lifecycle.LiveData.observeForever(LiveData.java:224)
        at androidx.lifecycle.MediatorLiveData$Source.plug(MediatorLiveData.java:141)
        at androidx.lifecycle.MediatorLiveData.addSource(MediatorLiveData.java:96)
        at com.example.pretest.schedule.pages.PagesViewModel.lambda$getPageCombinedData$2$PagesViewModel(PagesViewModel.java:59)
        at com.example.pretest.schedule.pages.-$$Lambda$PagesViewModel$Z3rbrO9SPtD8Pwpbh3mfk47DjVE.run(lambda)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1080)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:573)
        at java.lang.Thread.run(Thread.java:841)

just tell me plain and simple, is what I'm trying to achieve impossible ?


Solution

  • it appears that I don't to include my code inside any executer when using LiveData as return value from Dao queries.
    Room really execute queries on background only if it results from its abstract queries not the one I wrote.

    so my code looks like this at the end :

    my Dao

    @Dao
    public abstract class UserDao {
        @Query("Select * from page")
        public abstract LiveData<List<PageEntity>> getAllPages ();
        @Transaction
        @Query("Select * from user limit 1")
        public abstract LiveData<UserPageRelation> getAllUserPages ();   
    }
    

    my ViewModel

    public class MyViewModel extends ViewModel {
    
        private MediatorLiveData<PagesCombinedData> pagesCombinedData;
        public LiveData<PagesCombinedData> getPageCombinedData(){
               if (pagesCombinedData==null){
                   pagesCombinedData = new MediatorLiveData<>();
               }
               LiveData<List<PageEntity>> value1 = scheduleDB.userDao().getAllPages();
               LiveData<UserPageRelation> value2 = scheduleDB.userDao().getAllUserPages();
               pagesCombinedData.addSource(value1, value -> pagesCombinedData.setValue(combineData(value1,value2)));
               pagesCombinedData.addSource(value2, value -> pagesCombinedData.setValue(combineData(value1,value2)));
               return pagesCombinedData;
        }
        private PagesCombinedData combineData(LiveData<List<PageEntity>> pages,LiveData<UserPageRelation> userPages ){
            return new PagesCombinedData(pages.getValue()==null?new ArrayList<>():pages.getValue()
                    ,userPages.getValue()==null ?new ArrayList<>():userPages.getValue().getUserPages());
        }
    }
    

    and that work like charm returning the MediatorLiveData that I could observe from both of the data lists I wanted.