Paging Library: Saving data in the DB doesn't trigger the UI changes
Asked Answered
T

2

7

So, I've playing with Android's new Paging Library part of the JetPack tools.

I'm doing a very basic demo App where I show a list of random user profiles that I get from the RandomUser API.

The thing is that I have everything set and It's actually kinda working.

In my the list's fragment I'm listening for the DB changes:

...
mainViewModel.userProfiles.observe(this, Observer { flowableList ->
            usersAdapter.submitList(flowableList) })
...

(I tried with both, using Flowables and Observables with the RxPagedListBuilder and now I'm using LiveData with the LivePagedListBuilder. I'd like to use the RxPagedListBuilder if it's possible)

In my ViewModel I'm setting the LivePagedListBuilder

...
private val config = PagedList.Config.Builder()
        .setEnablePlaceholders(false)
        .setPrefetchDistance(UserProfileDataSource.PAGE_SIZE / 2) //Is very important that the page size is the same everywhere!
        .setPageSize(UserProfileDataSource.PAGE_SIZE)
        .build()

val userProfiles: LiveData<PagedList<UserProfile>> = LivePagedListBuilder(
        UserProfileDataSourceFactory(userProfileDao), config)
        .setBoundaryCallback(UserProfileBoundaryCallback(randomUserRepository, userProfileDao))
        .build()
...

I've set the DataSource to fetch the data form my DB (if there's any):

class UserProfileDataSource(val userDao: UserDao): PageKeyedDataSource<Int, UserProfile>() {

    companion object {
        const val PAGE_SIZE = 10
    }

    override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, UserProfile>) {
        callback.onResult(userDao.getUserProfileWithLimitAndOffset(PAGE_SIZE, 0), 0, PAGE_SIZE)
    }

    override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, UserProfile>) {
        callback.onResult(userDao.getUserProfileWithLimitAndOffset(PAGE_SIZE, params.key), params.key + PAGE_SIZE)
    }

    override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, UserProfile>) {
        //This one is not used
    }
}

And I've set the BoundaryCallback to fetch the data from network if the database is empty:

class UserProfileBoundaryCallback(val userRepository: RandomUserRepository,
                                  val userDao: UserDao) : PagedList.BoundaryCallback<UserProfile>() {


    /**
     * Database returned 0 items. We should query the backend for more items.
     */
    override fun onZeroItemsLoaded() {
        userRepository.getUserProfiles(0, UserProfileDataSource.PAGE_SIZE)
            .subscribeOn(Schedulers.io()) //We shouldn't write in the db over the UI thread!
            .subscribe(object: DataSourceSubscriber<List<UserProfile>>() {
                override fun onResultNext(userProfiles: List<UserProfile>) {
                    userDao.storeUserProfiles(userProfiles)
                }
            })
    }

    /**
     * User reached to the end of the list.
     */
    override fun onItemAtEndLoaded(itemAtEnd: UserProfile) {
        userRepository.getUserProfiles(itemAtEnd.indexPageNumber + 1, UserProfileDataSource.PAGE_SIZE)
            .subscribeOn(Schedulers.io()) //We shouldn't write in the db over the UI thread!
            .subscribe(object: DataSourceSubscriber<List<UserProfile>>() {
                override fun onResultNext(userProfiles: List<UserProfile>) {
                    userDao.storeUserProfiles(userProfiles)
                }
            })
    }

}

The thing is that the Flowable, Observable or LiveData in the UI only gets triggered if the data is coming from the database, when the UserProfileDataSource returns some result from database. If the UserProfileDataSource doesn't return any result, the BoundaryCallback gets called, the request to the API is successfully done, the data gets stored in the database, but the Flowable never gets triggered. If I close the App and open it again, is gonna fetch the data that we just got from the database and then is going to display it properly.

I've tried every possible solution that I saw in all the articles, tutorials and github projects that I searched.

As I said I tried to change the RxPagedListBuilder to a LivePagedListBuilder.

When using RxPagedListBuilder I tried with both, Flowables and Observables.

I tried setting different configurations for the PagedList.Config.Builder using different Schedulers.

I'm really out of options here, I've been all day long struggling with this, any tip will be appreciated.

If you need more info about the code, just leave me a comment and I'll update the post.

Update:

I've submitted the full source code to my Github repo

Teen answered 29/5, 2018 at 19:38 Comment(2)
have you got to know the solution or reason as I am going through the same problem. I applied the trick but interested in knowing the actual problem.Krohn
@4gus71n, facing same issue. any update to this problem?Bind
T
4

Okay, I found the error:

I just removed the UserProfileDataSourceFactory and the UserProfileDataSource classes. Instead of that I added this method to my DAO:

@Query("SELECT * FROM user_profile")
fun getUserProfiles(): DataSource.Factory<Int, UserProfile>

And I'm building my RxPagedListBuilder like:

RxPagedListBuilder(userDao.getUserProfiles(), UserProfileDataSource.PAGE_SIZE)
        .setBoundaryCallback(UserProfileBoundaryCallback(randomUserApi, userDao))
        .buildObservable()

And that's it. I'm not sure why it doesn't work with the DataSource and the DataSourceFactory, if anyone can explain this oddly behaviour I'll mark that answer as accepted since my solution was more like a lucky shot.

One thing that is happening now though, is that when I load the data into the adapter, the adapter scrolls back to the top position.

Teen answered 30/5, 2018 at 13:24 Comment(8)
did you find any reason for this? I am wondering how updates on the database are populated to the list, when one implement DataSource/Factory himself. My daos will return liveData instances and I can't get my head arount on how to hand over the changes.Gandhi
@Gandhi I am stuck with same problem, Did you find out solution or reason behind such behaviour?Krohn
@Krohn unfortunately not. End up with my own paging implementation, although I am not happy about it.Gandhi
@Gandhi Can you post your customisation here or share an github repo?Krohn
@Krohn can't post company code here, sorry :( Would take some time to anonymize itGandhi
But basically, I have a 'Page' companion' class, which holds the live-data to a chunk of data from DB(from pos x to y). On every 'next' call I build a new page with next positions. The page holds the result. When a live-data of a page triggers, all results of all pages is added together using a linkedHashSet and returned to caller, through a mediator with setValue(). Moreover..all live-data's of all pages are added to the same mediator. Once the mediator is no longer used by the caller, all pages are gone, too.Gandhi
I doubt that this will help. Sorry that I don't have the time to give you more details.Gandhi
@Gandhi No problem. Thank you!Krohn
N
0

If you implement the DataSource , you need to call DataSource#invalidate() after operating room database every time.

Newel answered 2/8, 2019 at 6:54 Comment(2)
I am facing the same issue, I need to implement my own DataSource, as I need to add Ads in between the List of data. where do I invalidate when implementing a Datasource?Bind
@Bind did you find any solution?Microelement

© 2022 - 2024 — McMap. All rights reserved.