Reputation: 610
I'm building an app that uses ArticleBoundaryCallback to init a call to an API, and store the response in Room. I'm also listening to that table using LiveData, and displaying the items in a PagedListAdapter.
The problem is that every time new data is inserted in Room's(Article) table, the entire list gets refreshed.
Also, on config changes, the entire data seems to be fetched again(ViewModel doesn't retain it, the RecyclerView gets recreated).
On every insert the RecyclerView jumps(a few rows if it's new data inserted, or at the beginning if it replaces the new data with the old one).
The entire code is in this GitHub repo.
My classes are:
Article:
@Entity(tableName = "article",
indices={@Index(value="id")})public class Article {
@PrimaryKey(autoGenerate = false)
@SerializedName("_id")
@Expose
@NonNull
private String id;
@SerializedName("web_url")
@Expose
private String webUrl;
@SerializedName("snippet")
@Expose
private String snippet;
@SerializedName("print_page")
@Expose
private String printPage;
@SerializedName("source")
@Expose
private String source;
@SerializedName("multimedia")
@Expose
@Ignore
private List<Multimedium> multimedia = null;
Article DAO:
@Dao
public interface ArticleDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
long insert(Article article);
@Insert(onConflict = OnConflictStrategy.REPLACE)
void update(Article... repos);
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertArticles(List<Article> articles);
@Delete
void delete(Article... articles);
@Query("DELETE FROM article")
void deleteAll();
@Query("SELECT * FROM article")
List<Article> getArticles();
@Query("SELECT * FROM article")
DataSource.Factory<Integer, Article> getAllArticles();}
LocalCache (stores/retrieves from Room)
public class LocalCache {
private static final String TAG = LocalCache.class.getSimpleName();
private ArticleDao articleDao;
private Executor ioExecutor;
public LocalCache(AppDatabase appDatabase, Executor ioExecutor) {
this.articleDao = appDatabase.getArticleDao();
this.ioExecutor = ioExecutor;
}
public void insertAllArticles(List<Article> articleArrayList){
ioExecutor.execute(new Runnable() {
@Override
public void run() {
Log.d(TAG, "inserting " + articleArrayList.size() + " repos");
articleDao.insertArticles(articleArrayList);
}
});
}
public void otherFunction(ArrayList<Article> articleArrayList){
// TODO
}
public DataSource.Factory<Integer, Article> getAllArticles() {
return articleDao.getAllArticles();
}
AppRepository
public class AppRepository {
private static final String TAG = AppRepository.class.getSimpleName();
private static final int DATABASE_PAGE_SIZE = 20;
private Service service;
private LocalCache localCache;
private LiveData<PagedList<Article>> mPagedListLiveData;
public AppRepository(Service service, LocalCache localCache) {
this.service = service;
this.localCache = localCache;
}
/**
* Search - match the query.
*/
public ApiSearchResultObject search(String q){
Log.d(TAG, "New query: " + q);
// Get data source factory from the local cache
DataSource.Factory dataSourceFactory = localCache.getAllArticles();
// every new query creates a new BoundaryCallback
// The BoundaryCallback will observe when the user reaches to the edges of
// the list and update the database with extra data
ArticleBoundaryCallback boundaryCallback = new ArticleBoundaryCallback(q, service, localCache);
// Get the paged list
LiveData data = new LivePagedListBuilder(dataSourceFactory, DATABASE_PAGE_SIZE)
.setBoundaryCallback(boundaryCallback)
.build();
mPagedListLiveData = data;
ApiSearchResultObject apiSearchResultObject = new ApiSearchResultObject();
apiSearchResultObject.setArticles(data);
return apiSearchResultObject;
}
public DataSource.Factory getAllArticles() {
return localCache.getAllArticles();
}
public void insertAllArticles(ArrayList<Article> articleList) {
localCache.insertAllArticles(articleList);
}
}
ViewModel
public class DBArticleListViewModel extends ViewModel {
private AppRepository repository;
// init a mutable live data to listen for queries
private MutableLiveData<String> queryLiveData = new MutableLiveData();
// make the search after each new search item is posted with (searchRepo) using "map"
private LiveData<ApiSearchResultObject> repositoryResult = Transformations.map(queryLiveData, queryString -> {
return repository.search(queryString);
});
// constructor, init repo
public DBArticleListViewModel(@NonNull AppRepository repository) {
this.repository = repository;
}
// get my Articles!!
public LiveData<PagedList<Article>> articlesLiveData = Transformations.switchMap(repositoryResult, object ->
object.getArticles());
// get teh Network errors!
public LiveData<String> errorsLiveData = Transformations.switchMap(repositoryResult, object ->
object.getNetworkErrors());
// Search REPO
public final void searchRepo(@NonNull String queryString) {
this.queryLiveData.postValue(queryString);
}
// LAST Query string used
public final String lastQueryValue() {
return (String)this.queryLiveData.getValue();
}
Activity - observing from VM
DummyPagedListAdapter articleListAdapter = new DummyPagedListAdapter(this);
localDBViewModel = ViewModelProviders.of(this, Injection.provideViewModelFactory(this)).get(DBArticleListViewModel.class);
localDBViewModel.articlesLiveData.observe(this, pagedListLiveData ->{
Log.d(TAG, "articlesLiveData.observe size: " + pagedListLiveData.size());
if(pagedListLiveData != null)
articleListAdapter.submitList(pagedListLiveData);
});
recyclerView.setAdapter(articleListAdapter);
Adapter
public class DummyPagedListAdapter extends PagedListAdapter<Article, ArticleViewHolder> {
private final ArticleListActivity mParentActivity;
public DummyPagedListAdapter(ArticleListActivity parentActivity) {
super(Article.DIFF_CALLBACK);
mParentActivity = parentActivity;
}
@NonNull
@Override
public ArticleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(mParentActivity).inflate(R.layout.article_list_content, parent, false);
return new ArticleViewHolder(itemView);
}
@Override
public void onBindViewHolder(@NonNull ArticleViewHolder holder, int position) {
Article article = getItem(position);
if (article != null) {
holder.bindTo(article);
} else {
holder.clear();
}
}
}
DIFF
public static DiffUtil.ItemCallback<Article> DIFF_CALLBACK = new
DiffUtil.ItemCallback<Article>() {
@Override
public boolean areItemsTheSame(@NonNull Article oldItem, @NonNull
Article newItem) {
return oldItem.getId() == newItem.getId();
}
@Override
public boolean areContentsTheSame(@NonNull Article oldItem, @NonNull
Article newItem) {
return oldItem.getWebUrl() == newItem.getWebUrl();
}
};
I really need to solve this one.Thank you!
Upvotes: 4
Views: 1128
Reputation: 610
Yeah.. it took me a while but I solved it. As I thought, stupid issue: in the DIFF_CALLBACK used by the adapter to decide what to add and what to ignore from the observed dataset, I was using as a comparator oldItem.getId() == newItem.getId() which are strings!!! And of course the adapter was always getting "new values" and adding them..
Corrected DiffUtil.ItemCallback
public static DiffUtil.ItemCallback<Article> DIFF_CALLBACK = new DiffUtil.ItemCallback<Article>() {
@Override
public boolean areItemsTheSame(@NonNull Article oldItem, @NonNull Article newItem) {
return oldItem.getStoreOrder() == newItem.getStoreOrder();
}
@Override
public boolean areContentsTheSame(@NonNull Article oldItem, @NonNull Article newItem) {
return oldItem.getId().equals(newItem.getId()) && oldItem.getWebUrl().equals(newItem.getWebUrl());
}
};
I hope this will be a reminder to always pay attention event to the most basic things. I lost a bunch of time with this. Hope you won't :)
Upvotes: 6