Search code examples
androidandroid-architecture-components

Paging Library invalidating data source not working


Recently I was trying this:

I have a list of jobs backed by data source (I am using paging library) and each item in job list is having a save button, and that save button updates the status of the job from unsaved to saved (or vice versa) in database and once updated it invalidates the DataSource, now that invalidation should cause reload for the current page immediately, but that isn't happening.

I checked values in database they actually get updated but that isn't the case with the UI.

Code:

public class JobsPagedListProvider {

private JobListDataSource<JobListItemEntity> mJobListDataSource;

public JobsPagedListProvider(JobsRepository jobsRepository) {
    mJobListDataSource = new JobListDataSource<>(jobsRepository);
}

public LivePagedListProvider<Integer, JobListItemEntity> jobList() {
    return new LivePagedListProvider<Integer, JobListItemEntity>() {
        @Override
        protected DataSource<Integer, JobListItemEntity> createDataSource() {
            return mJobListDataSource;
        }
    };
}

public void setQueryFilter(String query) {
    mJobListDataSource.setQuery(query);
}
}

Here is my custom datasource:

public class JobListDataSource<T> extends TiledDataSource<T> {

private final JobsRepository mJobsRepository;
private final InvalidationTracker.Observer mObserver;


String query = "";

@Inject
public JobListDataSource(JobsRepository jobsRepository) {
    mJobsRepository = jobsRepository;

    mJobsRepository.setJobListDataSource(this);

    mObserver = new InvalidationTracker.Observer(JobListItemEntity.TABLE_NAME) {
        @Override
        public void onInvalidated(@NonNull Set<String> tables) {
            invalidate();
        }
    };

    jobsRepository.addInvalidationTracker(mObserver);
}

@Override
public boolean isInvalid() {
    mJobsRepository.refreshVersionSync();
    return super.isInvalid();
}

@Override
public int countItems() {
    return DataSource.COUNT_UNDEFINED;
}


@Override
public List<T> loadRange(int startPosition, int count) {
    return (List<T>) mJobsRepository.getJobs(query, startPosition, count);
}


public void setQuery(String query) {
    this.query = query;
}
}

Here is the code in JobsRepository that updates job from unsaved to saved:

public void saveJob(JobListItemEntity entity) {
    Completable.fromCallable(() -> {
        JobListItemEntity newJob = new JobListItemEntity(entity);
        newJob.isSaved = true;
        mJobDao.insert(newJob);
        Timber.d("updating entity from " + entity.isSaved + " to "
                + newJob.isSaved); //this gets printed in log
        //insertion in db is happening as expected but UI is not receiving new list
        mJobListDataSource.invalidate();
        return null;
    }).subscribeOn(Schedulers.newThread()).subscribe();
}

Here is the Diffing logic for job list:

private static final DiffCallback<JobListItemEntity> DIFF_CALLBACK =  new DiffCallback<JobListItemEntity>() {
    @Override
    public boolean areItemsTheSame(@NonNull JobListItemEntity oldItem, @NonNull JobListItemEntity newItem) {
        return oldItem.jobID == newItem.jobID;
    }

    @Override
    public boolean areContentsTheSame(@NonNull JobListItemEntity oldItem, @NonNull JobListItemEntity newItem) {
        Timber.d(oldItem.isSaved + " comp with" + newItem.isSaved);
        return oldItem.jobID == newItem.jobID
                && oldItem.jobTitle.compareTo(newItem.jobTitle) == 0
                && oldItem.isSaved == newItem.isSaved;
    }
};

JobListDataSource in JobRepository (only relevant portion is mentioned below):

public class JobsRepository {
//holds an instance of datasource
private JobListDataSource mJobListDataSource;

//setter
public void setJobListDataSource(JobListDataSource jobListDataSource) {
    mJobListDataSource = jobListDataSource;
}

}

getJobs() in JobsRepository:

public List<JobListItemEntity> getJobs(String query, int startPosition, int count) {
    if (!isJobListInit) {

        Observable<JobList> jobListObservable = mApiService.getOpenJobList(
                mRequestJobList.setPageNo(startPosition/count + 1)
                        .setMaxResults(count)
                        .setSearchKeyword(query));

        List<JobListItemEntity> jobs = mJobDao.getJobsLimitOffset(count, startPosition);

        //make a synchronous network call since we have no data in db to return
        if(jobs.size() == 0) {
            JobList jobList = jobListObservable.blockingSingle();
            updateJobList(jobList, startPosition);
        } else {
            //make an async call and return cached version meanwhile
            jobListObservable.subscribe(new Observer<JobList>() {
                @Override
                public void onSubscribe(Disposable d) {

                }

                @Override
                public void onNext(JobList jobList) {
                    updateJobList(jobList, startPosition);
                }

                @Override
                public void onError(Throwable e) {
                    Timber.e(e);
                }

                @Override
                public void onComplete() {

                }
            });
        }
    }

    return mJobDao.getJobsLimitOffset(count, startPosition);
}

updateJobList in jobsRepository:

private void updateJobList(JobList jobList, int startPosition) {
    JobListItemEntity[] jobs = jobList.getJobsData();
    mJobDao.insert(jobs);
    mJobListDataSource.invalidate();
}

Solution

  • After reading the source code of DataSource I realized this:

    1. A DataSource once invalidated will never become valid again.
    2. invalidate() says: If invalidate has already been called, this method does nothing.

    I was actually having a singleton of my custom DataSource (JobListDataSource) provided by JobsPagedListProvider, so when I was invalidating my DataSource in saveJob() (defined in JobsRepository), it was trying to get new DataSource instance (to fetch latest data by again calling loadRange() - that's how refreshing a DataSource works) but since my DataSource was singleton and it was already invalid so no loadRange() query was being made!

    So make sure you don't have a singleton of DataSource and invalidate your DataSource either manually (by calling invalidate()) or have a InvalidationTracker in your DataSource's constructor.

    So the final solution goes like this:

    Don't have a singleton in JobsPagedListProvider:

    public class JobsPagedListProvider {
    
    private JobListDataSource<JobListItemEntity> mJobListDataSource;
    
    private final JobsRepository mJobsRepository;
    
    public JobsPagedListProvider(JobsRepository jobsRepository) {
        mJobsRepository = jobsRepository;
    }
    
    public LivePagedListProvider<Integer, JobListItemEntity> jobList() {
        return new LivePagedListProvider<Integer, JobListItemEntity>() {
            @Override
            protected DataSource<Integer, JobListItemEntity> createDataSource() {
                //always return a new instance, because if DataSource gets invalidated a new instance will be required(that's how refreshing a DataSource works)          
                mJobListDataSource = new JobListDataSource<>(mJobsRepository);
                return mJobListDataSource;
            }
        };
    }
    
    public void setQueryFilter(String query) {
        mJobListDataSource.setQuery(query);
    }
    }
    

    Also make sure if you're fetching data from network you need to have right logic to check whether data is stale before querying the network else it will requery everytime the DataSource gets invalidated. I solved it by having a insertedAt field in JobEntity which keeps track of when this item was inserted in DB and checking if it is stale in getJobs() of JobsRepository.

    Here is the code for getJobs():

    public List<JobListItemEntity> getJobs(String query, int startPosition, int count) {
        Observable<JobList> jobListObservable = mApiService.getOpenJobList(
                mRequestJobList.setPageNo(startPosition / count + 1)
                        .setMaxResults(count)
                        .setSearchKeyword(query));
    
        List<JobListItemEntity> jobs = mJobDao.getJobsLimitOffset(count, startPosition);
    
        //no data in db, make a synchronous call to network to get the data
        if (jobs.size() == 0) {
            JobList jobList = jobListObservable.blockingSingle();
            updateJobList(jobList, startPosition, false);
        } else if (shouldRefetchJobList(jobs)) {
            //data available in db, so show a cached version and make async network call to update data
            jobListObservable.subscribe(new Observer<JobList>() {
                @Override
                public void onSubscribe(Disposable d) {
    
                }
    
                @Override
                public void onNext(JobList jobList) {
                    updateJobList(jobList, startPosition, true);
                }
    
                @Override
                public void onError(Throwable e) {
                    Timber.e(e);
                }
    
                @Override
                public void onComplete() {
    
                }
            });
        }
    
        return mJobDao.getJobsLimitOffset(count, startPosition);
    }
    

    Finally remove InvalidationTracker in JobListDatasource as we are handling invalidation manually:

    public class JobListDataSource<T> extends TiledDataSource<T> {
    
    private final JobsRepository mJobsRepository;
    
    String query = "";
    
    public JobListDataSource(JobsRepository jobsRepository) {
        mJobsRepository = jobsRepository;
        mJobsRepository.setJobListDataSource(this);
    }
    
    @Override
    public int countItems() {
        return DataSource.COUNT_UNDEFINED;
    }
    
    @Override
    public List<T> loadRange(int startPosition, int count) {
        return (List<T>) mJobsRepository.getJobs(query, startPosition, count);
    }
    
    
    public void setQuery(String query) {
        this.query = query;
    }
    }