How to Implement Paginate a query on android Firestore [duplicate] - android

This question already has answers here:
How to paginate Firestore with Android?
(3 answers)
Closed 4 years ago.
On working around to learn firebase firestore for an example from GitHub friendly eat app
I thought to implement pagination to limiting nodes for 10
private static final int LIMIT = 10;
in the firestore example app the mAdapter loads data/nodes as below
mFirestore = FirebaseFirestore.getInstance();
// Get ${LIMIT} restaurants
mQuery = mFirestore.collection("restaurants")
.orderBy("avgRating", Query.Direction.DESCENDING)
.limit(LIMIT);
// RecyclerView
mAdapter = new RestaurantAdapter(mQuery, this) {
#Override
protected void onDataChanged() {
// Show/hide content if the query returns empty.
if (getItemCount() == 0) {
mRestaurantsRecycler.setVisibility(View.GONE);
mEmptyView.setVisibility(View.VISIBLE);
} else {
mRestaurantsRecycler.setVisibility(View.VISIBLE);
mEmptyView.setVisibility(View.GONE);
}
}
#Override
protected void onError(FirebaseFirestoreException e) {
// Show a snackbar on errors
Snackbar.make(findViewById(android.R.id.content),
"Error: check logs for info.", Snackbar.LENGTH_LONG).show();
}
};
mRestaurantsRecycler.setLayoutManager(new LinearLayoutManager(this));
mRestaurantsRecycler.setAdapter(mAdapter);
// Filter Dialog
mFilterDialog = new FilterDialogFragment();
}
and
#Override
public void onStart() {
super.onStart();
// Start sign in if necessary
if (shouldStartSignIn()) {
startSignIn();
return;
}
// Apply filters
onFilter(mViewModel.getFilters());
// Start listening for Firestore updates
if (mAdapter != null) {
mAdapter.startListening();
}
}
on firestore docs says about to paginate
// Construct query for first 25 cities, ordered by population
Query first = db.collection("cities")
.orderBy("population")
.limit(25);
first.get()
.addOnSuccessListener(new OnSuccessListener<QuerySnapshot>() {
#Override
public void onSuccess(QuerySnapshot documentSnapshots) {
// ...
// Get the last visible document
DocumentSnapshot lastVisible =
documentSnapshots.getDocuments()
.get(documentSnapshots.size() -1);
// Construct a new query starting at this document,
// get the next 25 cities.
Query next = db.collection("cities")
.orderBy("population")
.startAfter(lastVisible)
.limit(25);
// Use the query for pagination
// ...
}
});
combining those above codes how should I implement paginate to load more than 10
nodes to load when I scroll to the bottom of the recycler view
// Use the query for pagination
// ...
Update: I am working based on firestore doc about Paginate query and taking look at a possible duplicate of another question I did not get it to done working
Thank you

Here I found solution around but not better one, if is there any better way please post
by saving RecyclerView state before loading more nodes and reloading RecyclerView state after increasing the limit
private static final int LIMIT = 10;
Changed to
private int LIMIT = 10;
when recyclerView is scrolled to the bottom
mRestaurantsRecycler.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
final int mLastVisibleItemPosition = mManager.findLastVisibleItemPosition();
if ( mLastVisibleItemPosition == (LIMIT-1)) {
LIMIT = LIMIT*2;
showSpotDialog();
// save RecyclerView state
mBundleRecyclerViewState = new Bundle();
Parcelable listState = mRestaurantsRecycler.getLayoutManager().onSaveInstanceState();
mBundleRecyclerViewState.putParcelable(KEY_RECYCLER_STATE, listState);
loadMore(query);
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
// restore RecyclerView state
if (mBundleRecyclerViewState != null) {
Parcelable listState = mBundleRecyclerViewState.getParcelable(KEY_RECYCLER_STATE);
mRestaurantsRecycler.getLayoutManager().onRestoreInstanceState(listState);
}
hideSpotDialog();
}
}, 500);
}
}
});
it looks very unusual when nodes are loaded after the limit, but no way for now...
and yes I am looking for loading more nodes without flaws

I have extended the FirestoreAdapter as followed:
Keep track DocumentSnapshots identifier.
private Set<String> mIdentifier = new HashSet<>();
Add a new public method for pagination, as i expect the rest of the query to remain the same, the given query does not need to be changed
/**
* Extends the query to load even more data rows. This method will do nothing if the query has
* not yet been set.
* #param limit the new limit
*/
public void paginate(long limit) {
if (mQuery != null) {
if (mRegistration != null) {
mRegistration.remove();
mRegistration = null;
}
// Expect the query to stay the same, only the limit will change
mQuery = mQuery.limit(limit);
startListening();
}
}
Clear the identifier in the setQuery(Query) method by calling mIdentifier.clear()
Adopt the onDocumentAdded(DocumentChange) and the onDocumentRemoved(DocumentChange) as followed
protected void onDocumentAdded(DocumentChange change) {
if (!mIdentifier.contains(change.getDocument().getId())) {
mSnapshots.add(change.getNewIndex(), change.getDocument());
mIdentifier.add(change.getDocument().getId());
notifyItemInserted(change.getNewIndex());
}
}
protected void onDocumentRemoved(DocumentChange change) {
mSnapshots.remove(change.getOldIndex());
mIdentifier.remove(change.getDocument().getId());
notifyItemRemoved(change.getOldIndex());
}
For the onScrolling listener i stick to this guide: Endless Scrolling with AdapterViews and RecyclerView

Related

fetching data for the second time increments my long variable by 1, causing it not possible to reload data more than once

I have a few data fetching methods that call each other in order to fetch data according to the previous data fetched.
My issue is that I am holding a long variable called 'allContestsCounterIndex' which is incremented only at 2 certain places and for some reason after fetching the data for the first time, when clicking again to try to fetch for the second time the value of it starts with 1, meaning I can't continue because it makes the logic stuck.
here are the functions I am using -
private void updateContestCallCounter(long childrenCount) {
allContestsCounterIndex++;
if (allContestsCounterIndex == (childrenCount - 1)) { // at this point at the second time this variable starts with a value of 1 instead of 0
fetchWinnersFromOurValidContests();
}
}
private void fetchAllEndedStatusContests(DataSnapshot dataSnapshot) {
if (!dataSnapshot.exists()) {
// For some reason, we've reached this point with no dataSnapShot
Timber.d("dataSnapShot doesnt exists");
return;
}
long timeInSeconds = (System.currentTimeMillis() / 1000);
mEndedContests = new ArrayList<>();
long childrenCount = dataSnapshot.getChildrenCount();
for (DataSnapshot status : dataSnapshot.getChildren()) {
//fetching the status key for deeper querys inside the db
final String key = status.getKey();
checkIfContestEnded(timeInSeconds, key, new OnContestStateReceived() {
#Override
public void contestHasEnded() {
mEndedContests.add(key);
updateContestCallCounter(childrenCount);
}
#Override
public void contestStillScheduled() {
updateContestCallCounter(childrenCount);
}
});
}
}
You should set all of your counters to 0 once you have finished re-fetching the data.

How to update all live data the same time?

I want to update my live data(s) in my ViewModel when the app detects the user's filters changed on a drawer layout close listener. I have created an update all live data method in my ViewModel, but it doesn't seem to work.
Here's my ViewModel:
public class ReleasesViewModel extends ViewModel {
private HashMap<String, MutableLiveData<List<_Release>>> upcomingReleasesListMap = new HashMap<>();
private ReleasesRepository releasesRepository;
private ArrayList<Integer> platforms;
private ArrayList<Integer> platformsCopy;
private String region;
public ReleasesViewModel() {
upcomingReleasesListMap = new HashMap<>();
// Shared to all fragments : User settings region & platforms
region = SharedPrefManager.read(SharedPrefManager.KEY_PREF_REGION, "North America");
Set<String> defaultPlatformsSet = new HashSet<>();
platforms = SharedPrefManager.read(SharedPrefManager.PLATFORM_IDS);
if (platformsCopy == null) {
// Set only once [past copy of platforms]
platformsCopy = SharedPrefManager.read(SharedPrefManager.PLATFORM_IDS);
}
}
public MutableLiveData<List<_Release>> getUpcomingReleases(String filter) {
// ReleasesRepository takes a different monthly filter
if (upcomingReleasesListMap.get(filter) == null) {
// we don't have a mapping for this filter so create one in the map
MutableLiveData<List<_Release>> releases = new MutableLiveData<>();
upcomingReleasesListMap.put(filter, releases);
// also call this method to update the LiveData
loadReleases(filter);
} else if (upcomingReleasesListMap.containsKey(filter)) {
// Double check if it isn't null, just in case
if (upcomingReleasesListMap.get(filter) == null || isPlatformsUpdated()) {
// if null; try again to update the live data or if platforms filter changed
loadReleases(filter);
} // else just don't do anything, the list is already in the Map
}
return upcomingReleasesListMap.get(filter);
}
private void loadReleases(final String filter) {
releasesRepository = new ReleasesRepository(region, filter, platforms);
releasesRepository.addListener(new FirebaseDatabaseRepository.FirebaseDatabaseRepositoryCallback<_Release>() {
#Override
public void onSuccess(List<_Release> result) {
// sort by release date
if (platforms.size() > 1) {
// Will only sort for multiple platforms filter
Collections.sort(result);
}
// just use the previous created LiveData, this time with the data we got
MutableLiveData<List<_Release>> releases = upcomingReleasesListMap.get(filter);
releases.setValue(result);
}
#Override
public void onError(Exception e) {
// Log.e(TAG, e.getMessage());
MutableLiveData<List<_Release>> releases = upcomingReleasesListMap.get(filter);
releases.setValue(null);
}
});
}
// Detects when user added/removed platform to update lists based on platforms
private boolean isPlatformsUpdated() {
Collections.sort(platforms);
Collections.sort(platformsCopy);
if (platforms.equals(platformsCopy)) {
// nothing new added since past update
return false;
}
// something new added or removed, change
platformsCopy = SharedPrefManager.read(SharedPrefManager.PLATFORM_IDS);
return true;
}
public void updateAllReleasesList() {
// update all releases live data lists
for (String filter : upcomingReleasesListMap.keySet()) {
loadReleases(filter);
}
}
}
The updateAllReleasesList is the method I created to update all my livedata lists, but in calling this method it will call the loadReleases method again and inside this method, it will skip the entire listener addListener code for some reason.
In my fragments where I listen to the data changes I have this following simple observer:
mReleasesViewModel = ViewModelProviders.of(getActivity()).get(ReleasesViewModel.class);
mReleasesViewModel.getUpcomingReleases(filter).observe(this, new Observer<List<_Release>>() {
#Override
public void onChanged(#Nullable List<_Release> releases) {
// whenever the list is changed
if (releases != null) {
mUpcomingGamesAdapter.setData(releases);
mUpcomingGamesAdapter.notifyDataSetChanged();
Toast.makeText(getContext(), "Updated", Toast.LENGTH_SHORT).show();
}
mDatabaseLoading.setVisibility(View.GONE);
}
});
And when I call my update all method in my ViewModel on drawer close, the code inside the observer in my fragment gets called (the list returned is empty), but I want it to call getUpcomingReleases to update everything.. Any ideas on how to update all my current livedatas at the same time and reflect it on the UI?

Paging library - populate from cache while requesting from network

I have to load cached version of data from database and simultaneously I want to make a request to server for fresh data and I want to do this on per page basis.
So, for example for first page I want to show a cached version of first page data from database while requesting fresh data only for first page.
I want to achieve this using Paging Library.
I tried creating custom data source which helped me intercept page load request which then I used to make a network call with required page number and limit and meanwhile I returned a cached version from db, the problem is after getting fresh data from network I update the database but those updates are not reflected.
(I believe the whole table is being observed for any modifications using Invalidation Tracker and data source is invalidated whenever tables are invalidated, I added that tracker in my data source too but still it ain't working; I was able to make out that Invalidation Tracker thing by temporarily creating: LivePagedListProvider getJobs() in JobDao and checking generated implementation)
Code:
public class JobListDataSource<T> extends TiledDataSource<T> {
private final JobsRepository mJobsRepository;
private final InvalidationTracker.Observer mObserver;
String query = "";
public JobListDataSource(JobsRepository jobsRepository) {
mJobsRepository = jobsRepository;
mObserver = new InvalidationTracker.Observer(JobEntity.TABLE_NAME) {
#Override
public void onInvalidated(#NonNull Set<String> tables) {
invalidate();
}
};
jobsRepository.addInvalidationTracker(mObserver);
}
#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;
}
}
Jobs repository functions:
public List<JobEntity> getJobs(String query, int startPosition, int count) {
if (!isJobListInit) {
JobList jobList = mApiService.getOpenJobList(
mRequestJobList.setPageNo(startPosition/count + 1)
.setMaxResults(count)
.setSearchKeyword(query)
).blockingSingle();
mJobDao.insert(jobList.getJobsData());
}
return mJobDao.getJobs(startPosition, count);
}
public void addInvalidationTracker(InvalidationTracker.Observer observer) {
mAppDatabase.getInvalidationTracker().addObserver(observer);
}
So I understood why it wasn't working, there was a mistake at my end, I was passing wrong parameters to getJobs method of JobDao in JobsRepository.
The getJobs method of JobDao goes as follows:
#Query("SELECT * FROM jobs ORDER BY jobID ASC LIMIT :limit OFFSET :offset")
List<JobEntity> getJobs(int limit, int offset);
And the call getJobs() in JobsRepository goes as follows:
return mJobDao.getJobs(startPosition, count);
So the first parameter was the limit and the next one was the offset but I was passing other way around.
Now it works like a charm!
Furthermore, I made a change to getJobs() in JobsRepository:
First get data from db, if available return and make an async request to network if required.
If no data is available in db, make a synchronous call to network, get data from network, parse it and save it db and now access latest data from db and return it.
So the function goes like this:
//you can even refactor this code so that all the network related stuff is in one class and just call that method
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 (shouldFetchJobList(jobs)) {
//data available in db, so show a cached version and make async network call to update data as this data is no longer fresh
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);
}
updateJobList() code:
private void updateJobList(JobList jobList, int startPosition, boolean performInvalidation) {
JobListItemEntity[] jobs = jobList.getJobsData();
Date currentDate = Calendar.getInstance().getTime();
//tracks when this item was inserted in db, used in calculating whether data is stale
for (int i = 0; i < jobs.length; i++) {
jobs[i].insertedAt = currentDate;
}
mJobDao.insert(jobs);
if (performInvalidation) {
mJobListDataSource.invalidate();
}
}
(I also renamed the getJobs() in JobDao to getJobsLimitOffset() as it makes it more readable and that is also the way methods are generated by paging library)

FirebaseUI RecyclerView random items added to listitems

When a button is pressed, a dialog appears asking user for message, with the option of attaching an image (from url). The problem I'm having is once the recyclerview is filled with enough items to scroll, when the user scrolls quickly for some reason random images start popping up in seemingly random list items.
I know the problem has to come from when the image is actually placed into the imageview, since I can tell the link is added to the firebase db just fine.
When the image link is submitted, it's sent to /posts/$uid/$post-id in a HashMap. Kind of like this:
final Map<String, String> postMap = new HashMap<String, String>();
imagebutton.setOnClickListener((view) -> {
AlertDialog.Builder = new...
LayoutInflater in = ...
View dialogLayout = ...inflate(r.layout...., null);
build.setView(dialogLayout);
EditText imgText = ...
Button submit = ...
AlertDialog a = build.create();
submit.setOnClickListener((View) -> {
...
postMap.put("imgLink", imgText.getText().toString());
a.dismiss();
...
urlDialog.show();
Then a few more items are added to the map and pushed to firebase.
Firebase postRef = ref.child("posts").child(auth.getUid());
postMap.put("author", ...);
postMap.put("content", ...);
postRef.push().setValue(postMap);
But like I said, I'm almost 100% sure the problem is not in posting the information, just populating the recview
Here's my code for the list itself:
RecyclerView feed = (RecyclerView)findViewById(R.id.recycler);
if (ref.getAuth() != null) {
FirebaseRecyclerAdapter<TextPost, PostViewHolder> adapter = new FirebaseRecyclerAdapter<TextPost, PostViewHolder>(TextPost.class, R.layout.list_item, PostViewHolder.class, ref.child("posts").child(uid)) {
#Override
protected void populateViewHolder(final PostViewHolder postViewHolder, final TextPost textPost, int i) {
postViewHolder.content.setText(textPost.getContent());
postViewHolder.author.setText(textPost.getAuthor());
postViewHolder.score.setText(textPost.getScore());
postViewHolder.time.setText(textPost.getTime());
if (textPost.getImgLink() != null && !textPost.getImgLink().equals("")) {
Log.i(TAG, "Setting image");
new Thread(new Runnable() {
#Override
public void run() {
try {
final Bitmap pic = bitmapFromUrl(textPost.getImgLink());
postViewHolder.img.post(new Runnable() {
#Override
public void run() {
postViewHolder.img.setImageBitmap(pic);
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
...
feed.setAdapter(adapter);
I just started learning how to work with worker threads for network activities off the main UI thread so I assume I messed that up? I've gone through the logic over and over in my head and i can't seem to figure out what's going wrong here.
EDIT: I tried using AsyncTask instead of Threads and the problem persists. sos
All I had to do was set the ImageView drawable to null before populating the ImageView.
Like this:
#Override
protected void populateViewHolder(final ViewHolder v, final Object o, int i) {
//populate views
v.content.setText("...");
//Set imageview to null
v.imageview.setImageDrawable(null);*
if (o.getImageLink() != null && !o.getImageLink.equals("")) {
// Start AsyncTask to get image from link and populate imageview
new DownloadImageTask().execute(o.getImgLink(), v.imageview);
}
}

Using popBackStack() in Android does not update android-listview with Firebase data

At the beginning of the chat app user see a list off groups (listview group) available and the user have the possibility to create a new group or click on some off the available groups and then start to write messages (listview messages). The functions CreateNewMessage and CreateNewGroup pushes information to firebase correctly
Above scenarios works finne problems arise when user navigates backwards (popBackStack()) from listview with messages to GroupFragment, here should user be presented a list off available groups but the listview is empty. The ReadGroupData() function is not reading the already created groups from firebase and inserts them in the group listview. How to make this happen?
GroupFragment:
public void ReadGroupData() {
Firebase firebaserootRef = new Firebase("https://000.firebaseio.com");
firebaserootRef.addChildEventListener(new ChildEventListener() {
#Override
public void onChildAdded(DataSnapshot snapshot, String s) {
if (snapshot.getValue() != null) {
Group newGroup = new Group((String)snapshot.child("name").getValue(),
(String) snapshot.child("id").getValue());
if(!groupKeyValues.contains(newGroup.GetId())) {
groupKeyValues.add(newGroup.GetId());
AddToLstViewGroup(newGroup);
System.out.println("Read group data from firebase and
inserted in listView");
}
}
}
});
}
public void AddToLstViewGroup(Group newGroup) {
groupNameList.add(newGroup);
if(groupAdapter == null) {
groupAdapter = new GroupAdapter(getActivity(), groupNameList);
}
if (lstViewGroup == null) {
lstViewGroup = (ListView) getView().
findViewById(R.id.listView_group);
}
lstViewGroup.setOnItemClickListener(onItemClickListener);
lstViewGroup.setOnItemLongClickListener(onItemLongClickListener);
groupAdapter.notifyDataSetChanged();
lstViewGroup.setAdapter(groupAdapter);
}
ChatFragment:
public void ReadChatMessages(Firebase firebaseRootRef) {
firebaseRootRef.addChildEventListener(new ChildEventListener() {
#Override
public void onChildAdded(DataSnapshot snapshot, String s) {
if (snapshot.child(GetGroupId()).child("messages").
getChildren() != null) {
for (DataSnapshot c :
snapshot.child(GetGroupId()).child("messages").getChildren()) {
String key = c.getKey();
Message newMessage = new Message();
newMessage.SetFrom((String) c.child("from").getValue());
newMessage.SetMsg((String)
c.child("message").getValue());
newMessage.SetTime((String) c.child("time").getValue());
newMessage.SetId((String) c.child("id").getValue());
if ((!msgKeyValues.contains(key)) ||
newMessage.GetFrom() != "") {
msgKeyValues.add(key);
AddToLstViewChat(newMessage);
//Automatic scrolls to last line in listView.
lstViewChat.setSelection(chatAdapter.getCount() -1);
}
}
}
}
public void AddToLstViewChat(Message newMessage) {
chatMsgList.add(newMessage);
if (chatAdapter == null) {
chatAdapter = new ChatAdapter(getActivity(), chatMsgList);
}
if(IsMsgFromMe(newMessage)) {
lstViewChat = (ListView)
getView().findViewById(R.id.listView_chat_message_me);
} else {
lstViewChat =
(ListView)getView().findViewById(R.id.listView_chat_message_others);
}
chatAdapter.notifyDataSetChanged();
lstViewChat.setAdapter(chatAdapter);
}
ChatActivity:
#Override
public void onBackPressed() {
if (getFragmentManager().getBackStackEntryCount() > 0) {
getFragmentManager().popBackStack();
} else {
finish();
}
}
For all the code click on the link: "http://pastebin.com/97nR68Rm"
SOLUTION!
Kato thank you for you patience and help. I have now found a solution for the problem. I'm calling ReadGroupData() and ReadChatMessages() at the end (before return) in my onCreateView methods. As Kato pointed out onCreate() is not getting called on popBackStack()
In my AddToLStViewGroup the if statement for lstViewGroup is deleted so now it always sets the listView otherwise it will throw an exception for not finding the correct view, To clarifying:
Deleted this line:
if (lstViewGroup == null) {
lstViewGroup = (ListView)getView().findViewById(R.id.listView_group);
}
And replaced with:
ListView lstViewGroup=(ListView)getView().findViewById(R.id.listView_group);
Kato thank you for you patience and help. I have now found a solution for the problem. I'm calling ReadGroupData() and ReadChatMessages() at the end (before return) in my onCreateView methods. As Kato pointed out onCreate() is not getting called on popBackStack()
In my AddToLStViewGroup the if statement for listViewGroup is deleted so now it always sets the listView otherwise it will throw an exception for not finding the correct view.
To clarify:
I deleted this line:
if (lstViewGroup == null) {
lstViewGroup = (ListView)getView().findViewById(R.id.listView_group);
}
And replaced it with:
ListView lstViewGroup =(ListView)getView().findViewById(R.id.listView_group);
(The original asker posted the answer as part of the question. I'm copying it here as a matter of housekeeping.)

Categories

Resources