I'm following the tutorial:
https://codelabs.developers.google.com/codelabs/firebase-android/#6
I was trying to achieve pagination, Here is what i have modified:
...
Query query = databaseRef.limitToLast(50);
...
FirebaseRecyclerOptions<FriendlyMessage> options =
new FirebaseRecyclerOptions.Builder<FriendlyMessage>()
.setQuery(query, parser)
.build();
...
Here is the scrolling code as tutorial as default:
mFirebaseAdapter.registerAdapterDataObserver(new
RecyclerView.AdapterDataObserver() {
#Override
public void onItemRangeInserted(int positionStart, int itemCount) {
super.onItemRangeInserted(positionStart, itemCount);
int friendlyMessageCount = mFirebaseAdapter.getItemCount();
int lastVisiblePosition =
mLinearLayoutManager.findLastCompletelyVisibleItemPosition();
// If the recycler view is initially being loaded or the
// user is at the bottom of the list, scroll to the bottom
// of the list to show the newly added message.
if (lastVisiblePosition == -1 ||
(positionStart >= (friendlyMessageCount - 1) &&
lastVisiblePosition == (positionStart - 1))) {
mMessageRecyclerView.scrollToPosition(positionStart);
}
}
});
mMessageRecyclerView.setAdapter(mFirebaseAdapter);
Now the screen shows only 50 messages.
But it don't scroll to the bottom when new messages coming.It works fine before using query.
I want to know where should I start to modified.
Thank you.
From the swift side:
By changing the startKey, we can query the data from where we want(from the end of the database) and achieve the pagination by scrolling to the top of the screen.
if (startKey == nil){
print("firebasetest_startkey: ",self.startKey)
// for first grabbing data operation
_refHandle = self.ref.child(channel_title).queryOrderedByKey().queryLimited(toLast: 30).observe(.value){(snapshot) in
guard let children = snapshot.children.allObjects.first as? DataSnapshot else{return}
if (snapshot.childrenCount > 0){
for child in snapshot.children.allObjects as! [DataSnapshot]{
if(!(self.messageKeys.contains((child as AnyObject).key))){
self.messages.append(child)
self.messageKeys.append(child.key)
self.itemTable.insertRows(at: [IndexPath(row: self.messages.count-1, section: 0)], with: .automatic)
}
}
self.startKey = children.key
}
}
}else if (dragdirection == 0 && startKey != nil){
//going up
// for all other grabbing data operation from the bottom of the database
_refHandle = self.ref.child(channel_title).queryOrderedByKey().queryEnding(atValue: self.startKey).queryLimited(toLast: 10).observe(.value){(snapshot) in
guard let children = snapshot.children.allObjects.first as? DataSnapshot else{return}
if (snapshot.childrenCount > 0 ){
for child in snapshot.children.reversed(){
if ((child as AnyObject).key != self.startKey &&
!(self.messageKeys.contains((child as AnyObject).key))){
self.messages.insert(child as! DataSnapshot, at:0)
self.messageKeys.append((child as AnyObject).key)
self.itemTable.insertRows(at: [IndexPath(row: 0, section: 0)], with: .fade)
}
}
self.startKey = children.key
}
}
}
Related
I'm now trying to call paging data from the database using remotemediator.
It does not insert into the ROOM if the item on the page queried from the network is already read.
The problem is that if all the items on the page queried from the network were already read, there is no trigger to query the next page because pagingState.firstItemOrNull() == null and pagingState.lastItemOrNull() == null.
Is there a way to requery with loadkey +1 if all the query results have been read?
How can I solve this problem?
switch (loadType) {
case REFRESH:
loadKey = initialKey;
keysFuture = keysDao.getRemoteKeyFuture(-1);
break;
case PREPEND:
if(pagingState.firstItemOrNull() == null){
return Futures.immediateFuture(new MediatorResult.Success(false));
}
keysFuture = keysDao.getRemoteKeyFuture(pagingState.firstItemOrNull().getId());
break;
case APPEND:
if(pagingState.lastItemOrNull() == null){
return Futures.immediateFuture(new MediatorResult.Success(false));
}
keysFuture = keysDao.getRemoteKeyFuture(pagingState.lastItemOrNull().getId());
break;
}
I am working with Firestore right now and have a little bit of a problem with pagination.
Basically, I have a collection (assume 10 items) where each item has some data and a timestamp.
Now, I am fetching the first 3 items like this:
Firestore.firestore()
.collection("collectionPath")
.order(by: "timestamp", descending: true)
.limit(to: 3)
.addSnapshotListener(snapshotListener())
Inside my snapshot listener, I save the last document from the snapshot, in order to use that as a starting point for my next page.
So, at some time I will request the next page of items like this:
Firestore.firestore()
.collection("collectionPath")
.order(by: "timestamp", descending: true)
.start(afterDocument: lastDocument)
.limit(to: 3)
.addSnapshotListener(snapshotListener2()) // Note that this is a new snapshot listener, I don't know how I could reuse the first one
Now I have the items from index 0 to index 5 (in total 6) in my frontend. Neat!
If the document at index 4 now updates its timestamp to the newest timestamp of the whole collection, things start to go down.
Remember that the timestamp determines its position on account of the order clause!
What I expected to happen was, that after the changes are applied, I still show 6 items (and still ordered by their timestamps)
What happened was, that after the changes are applied, I have only 5 items remaining, since the item that got pushed out of the first snapshot is not added to the second snapshot automatically.
Am I missing something about Pagination with Firestore?
EDIT: As requested, I post some more code here:
This is my function to return a snapshot listener. Well, and the two methods I use to request the first page and then the second page I posted already above
private func snapshotListener() -> FIRQuerySnapshotBlock {
let index = self.index
return { querySnapshot, error in
guard let snap = querySnapshot, error == nil else {
log.error(error)
return
}
// Save the last doc, so we can later use pagination to retrieve further chats
if snap.count == self.limit {
self.lastDoc = snap.documents.last
} else {
self.lastDoc = nil
}
let offset = index * self.limit
snap.documentChanges.forEach() { diff in
switch diff.type {
case .added:
log.debug("added chat at index: \(diff.newIndex), offset: \(offset)")
self.tVHandler.dataManager.insert(item: Chat(dictionary: diff.document.data() as NSDictionary), at: IndexPath(row: Int(diff.newIndex) + offset, section: 0), in: nil)
case .removed:
log.debug("deleted chat at index: \(diff.oldIndex), offset: \(offset)")
self.tVHandler.dataManager.remove(itemAt: IndexPath(row: Int(diff.oldIndex) + offset, section: 0), in: nil)
case .modified:
if diff.oldIndex == diff.newIndex {
log.debug("updated chat at index: \(diff.oldIndex), offset: \(offset)")
self.tVHandler.dataManager.update(item: Chat(dictionary: diff.document.data() as NSDictionary), at: IndexPath(row: Int(diff.oldIndex) + offset, section: 0), in: nil)
} else {
log.debug("moved chat at index: \(diff.oldIndex), offset: \(offset) to index: \(diff.newIndex), offset: \(offset)")
self.tVHandler.dataManager.move(item: Chat(dictionary: diff.document.data() as NSDictionary), from: IndexPath(row: Int(diff.oldIndex) + offset, section: 0), to: IndexPath(row: Int(diff.newIndex) + offset, section: 0), in: nil)
}
}
}
self.tableView?.reloadData()
}
}
So again, I am asking if I can have one snapshot listener that listens for changes in more than one page I requested from Firestore
Well, I contacted the guys over at Firebase Google Group for help, and they were able to tell me that my use case is not yet supported.
Thanks to Kato Richardson for attending to my problem!
For anyone interested in the details, see this thread
I came across the same use case today and I have successfully implemented a working solution in Objective C client. Below is the algorithm if anyone wants to apply in their program and I will really appreciate if google-cloud-firestore team can put my solution on their page.
Use Case: A feature to allow paginating a long list of recent chats along with the option to attach real time listeners to update the list to have chat with most recent message on top.
Solution: This can be made possible by using pagination logic like we do for other long lists and attaching real time listener with limit set to 1:
Step 1: On page load fetch the chats using pagination query as below:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
[self fetchChats];
}
-(void)fetchChats {
__weak typeof(self) weakSelf = self;
FIRQuery *paginateChatsQuery = [[[self.db collectionWithPath:MAGConstCollectionNameChats]queryOrderedByField:MAGConstFieldNameTimestamp descending:YES]queryLimitedTo:MAGConstPageLimit];
if(self.arrChats.count > 0){
FIRDocumentSnapshot *lastChatDocument = self.arrChats.lastObject;
paginateChatsQuery = [paginateChatsQuery queryStartingAfterDocument:lastChatDocument];
}
[paginateChatsQuery getDocumentsWithCompletion:^(FIRQuerySnapshot * _Nullable snapshot, NSError * _Nullable error) {
if (snapshot == nil) {
NSLog(#"Error fetching documents: %#", error);
return;
}
///2. Observe chat updates if not attached
if(weakSelf.chatObserverState == ChatObserverStateNotAttached) {
weakSelf.chatObserverState = ChatObserverStateAttaching;
[weakSelf observeChats];
}
if(snapshot.documents.count < MAGConstPageLimit) {
weakSelf.noMoreData = YES;
}
else {
weakSelf.noMoreData = NO;
}
[weakSelf.arrChats addObjectsFromArray:snapshot.documents];
[weakSelf.tblVuChatsList reloadData];
}];
}
Step 2: On success callback of "fetchAlerts" method attach the observer for real time updates only once with limit set to 1.
-(void)observeChats {
__weak typeof(self) weakSelf = self;
self.chatsListener = [[[[self.db collectionWithPath:MAGConstCollectionNameChats]queryOrderedByField:MAGConstFieldNameTimestamp descending:YES]queryLimitedTo:1]addSnapshotListener:^(FIRQuerySnapshot * _Nullable snapshot, NSError * _Nullable error) {
if (snapshot == nil) {
NSLog(#"Error fetching documents: %#", error);
return;
}
if(weakSelf.chatObserverState == ChatObserverStateAttaching) {
weakSelf.chatObserverState = ChatObserverStateAttached;
}
for (FIRDocumentChange *diff in snapshot.documentChanges) {
if (diff.type == FIRDocumentChangeTypeAdded) {
///New chat added
NSLog(#"Added chat: %#", diff.document.data);
FIRDocumentSnapshot *chatDoc = diff.document;
[weakSelf handleChatUpdates:chatDoc];
}
else if (diff.type == FIRDocumentChangeTypeModified) {
NSLog(#"Modified chat: %#", diff.document.data);
FIRDocumentSnapshot *chatDoc = diff.document;
[weakSelf handleChatUpdates:chatDoc];
}
else if (diff.type == FIRDocumentChangeTypeRemoved) {
NSLog(#"Removed chat: %#", diff.document.data);
}
}
}];
}
Step 3. On listener callback check for document changes and handle only FIRDocumentChangeTypeAdded and FIRDocumentChangeTypeModified events and ignore the FIRDocumentChangeTypeRemoved event. We are doing this by calling "handleChatUpdates" method for both FIRDocumentChangeTypeAdded and FIRDocumentChangeTypeModified event in which we are first trying to find the matching chat document from local list and if it exist we are removing it from the list and then we are adding the new document received from listener callback and adding it to the beginning of the list.
-(void)handleChatUpdates:(FIRDocumentSnapshot *)chatDoc {
NSInteger chatIndex = [self getIndexOfMatchingChatDoc:chatDoc];
if(chatIndex != NSNotFound) {
///Remove this object
[self.arrChats removeObjectAtIndex:chatIndex];
}
///Insert this chat object at the beginning of the array
[self.arrChats insertObject:chatDoc atIndex:0];
///Refresh the tableview
[self.tblVuChatsList reloadData];
}
-(NSInteger)getIndexOfMatchingChatDoc:(FIRDocumentSnapshot *)chatDoc {
NSInteger chatIndex = 0;
for (FIRDocumentSnapshot *chatDocument in self.arrChats) {
if([chatDocument.documentID isEqualToString:chatDoc.documentID]) {
return chatIndex;
}
chatIndex++;
}
return NSNotFound;
}
Step 4. Reload the tableview to see the changes.
my solution is to create 1 maintainer query - listener to observe on those removed item from first query, and we will update it every time there's new message coming.
To make pagination with snapshot listener first we have to create reference point document from the collection.After that we are listening to collection based on that reference point document.
Let's you have a collection called messages and timestamp called createdAt with each document in that collection.
//get messages
getMessages(){
//first we will fetch the very last/latest document.
//to hold listeners
listnerArray=[];
const very_last_document= await this.afs.collectons('messages')
.ref
.limit(1)
.orderBy('createdAt','desc')
.get({ source: 'server' });
//if very_last.document.empty property become true,which means there is no messages
//present till now ,we can go with a query without having a limit
//else we have to apply the limit
if (!very_last_document.empty) {
const start = very_last_document.docs[very_last_document.docs.length - 1].data().createdAt;
//listner for new messages
//all new message will be registered on this listener
const listner_1 = this.afs.collectons('messages')
.ref
.orderBy('createdAt','desc')
.endAt(start) <== this will make sure the query will fetch up to 'start' point(including 'start' point document)
.onSnapshot(messages => {
for (const message of messages .docChanges()) {
if (message .type === "added")
//do the job...
if (message.type === "modified")
//do the job...
if (message.type === "removed")
//do the job ....
}
},
err => {
//on error
})
//old message will be registered on this listener
const listner_2 = this.afs.collectons('messages')
.ref
.orderBy('createdAt','desc')
.limit(20)
.startAfter(start) <== this will make sure the query will fetch after the 'start' point
.onSnapshot(messages => {
for (const message of messages .docChanges()) {
if (message .type === "added")
//do the job...
if (message.type === "modified")
//do the job...
if (message.type === "removed")
//do the job ....
}
this.listenerArray.push(listner_1, listner_2);
},
err => {
//on error
})
} else {
//no document found!
//very_last_document.empty = true
const listner_1 = this.afs.collectons('messages')
.ref
.orderBy('createdAt','desc')
.onSnapshot(messages => {
for (const message of messages .docChanges()) {
if (message .type === "added")
//do the job...
if (message.type === "modified")
//do the job...
if (message.type === "removed")
//do the job ....
}
},
err => {
//on error
})
this.listenerArray.push(listner_1);
}
}
//to load more messages
LoadMoreMessage(){
//Assuming messages array holding the the message we have fetched
//getting the last element from the array messages.
//that will be the starting point of our next batch
const endAt = this.messages[this.messages.length-1].createdAt
const listner_2 = this.getService
.collections('messages')
.ref
.limit(20)
.orderBy('createdAt', "asc") <== should be in 'asc' order
.endBefore(endAt) <== Getting the 20 documnents (the limit we have applied) from the point 'endAt';
.onSnapshot(messages => {
if (messages.empty && this.messages.length)
this.messages[this.messages.length - 1].hasMore = false;
for (const message of messages.docChanges()) {
if (message.type === "added")
//do the job...
if (message.type === "modified")
//do the job
if (message.type === "removed")
//do the job
}
},
err => {
//on error
})
this.listenerArray.push(listner_2)
}
I am working on a view controller where the user should delete a row of a sheet within a spreadsheet which is located in google drive.
Once the user has successfully logged in, a table view is displayed and the user can delete the row by swiping left on the appropriate cell. The code below shows the deletion process. When I trigger the process, the following error is displayed:
"Invalid requests[0].deleteRange: No grid with id: 0"
//Delete Function TableView - Access by swiping left.
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath)
{
//1 - Delete Row From Index
let toDelete = GTLRSheets_DeleteRangeRequest.init()
toDelete.range?.sheetId = 317088521
toDelete.range?.startRowIndex = 6
toDelete.range?.endRowIndex = 6
toDelete.range?.startColumnIndex = 0
toDelete.range?.endColumnIndex = 3
toDelete.shiftDimension = "ROWS"
let batchUpdate = GTLRSheets_BatchUpdateSpreadsheetRequest.init()
let request = GTLRSheets_Request.init()
request.deleteRange = toDelete
batchUpdate.requests = [request]
let deleteQuery = GTLRSheetsQuery_SpreadsheetsBatchUpdate.query(withObject: batchUpdate, spreadsheetId: spreadsheetID)
service.executeQuery(deleteQuery, delegate: self, didFinish: #selector(deleteFromIndexWithTicket(ticket:finishedWithObject:error:)))
}
func deleteFromIndexWithTicket(ticket: GTLRServiceTicket, finishedWithObject result : GTLRSheets_BatchGetValuesResponse , error : NSError?) {
if let error = error {
print(error.localizedDescription)
return
}
print("Deleted from index")
}
My bad... I had not initialised the GridRange. Below is the working code.
//1 - Delete Row From Index
let toDelete = GTLRSheets_DeleteRangeRequest.init()
let gridRange = GTLRSheets_GridRange.init()
toDelete.range = gridRange
gridRange.sheetId = indexID as NSNumber
gridRange.startRowIndex = lengthTrainingDatabase - indexPath.row as NSNumber
gridRange.endRowIndex = lengthTrainingDatabase - indexPath.row + 1 as NSNumber
toDelete.shiftDimension = kGTLRSheets_DeleteRangeRequest_ShiftDimension_Rows
I have a view pager and 3 fragment .
my app work good with 2 of them but when i add the third fragment its return this error :
12-17 14:11:30.969: ERROR/AndroidRuntime(7350): FATAL EXCEPTION: main
java.lang.RuntimeException: got asked about an unknown fragment
a part of my code is like this :
if (position == 0) {
if (mContactList == null)
mContactList = new ContactListFragment();
return mContactList;
} else if (position == 1) {
if (mContact == null)
mContact= new ContactFragment(); // my third fragment
return mContact ;
} else {
int positionMod = position - 2;
mCursor.moveToPosition(positionMod);
long contactChatId = mCursor.getLong(ChatView.CONTACT_ID_COLUMN);
String contactName = mCursor.getString(ChatView.USERNAME_COLUMN);
long providerId = mCursor.getLong(ChatView.PROVIDER_COLUMN);
return ChatViewFragment.newInstance(contactChatId, contactName, providerId);
}
i think you are trying to refer other fragments from your current fragment.
Try to set setOffscreenPageLimit to 2. As stated in the docs :
Set the number of pages that should be retained to either side of the current page in the view hierarchy in an idle state. Pages beyond this limit will be recreated from the adapter when needed.
Use this code :
ViewPager mViewPager = (ViewPager) findViewById(R.id.historypager);
mViewPager.setOffscreenPageLimit(2);
I am reading UDP packets and i wanna display that info on UI as table in android app.
Here is my code,
try {
byte buffer[] = new byte[10000];<br/>
InetAddress address = InetAddress.getByName("192.168.xx.xx");<br/>
int port = xxx;<br/>
Log.d("..........","What will Happen ?? ");<br/>
for(int k=0;k<50;k++) { // 50 rows are added , This i wanna make it 5000+ rows so it takes plenty of time to load that table <br/>
DatagramPacket p = new DatagramPacket(buffer, buffer.length, address, port);<br/>
DatagramSocket ds = new DatagramSocket(port);<br/>
Log.d("..........","Perfect Binding .... Waiting for Data");<br/>
ds.receive(p);<br/>
Log.d("..........","Packet Received");<br/>
byte[] data = p.getData();<br/>
String result = "";<br/>
int b[] = new int[data.length];</br>
for (int i=0; i < 150; i++) {<br/>
result += Integer.toString( ( data[i] & 0xff ) + 0x100, 16).substring( 1 );<br/>
result += "_";<br/>
}<br/>
Log.d("Result => ",result); <br/>
TableLayout tl=(TableLayout)findViewById(R.id.TableLayout01);<br/>
TableRow tr=new TableRow(this);
TextView tv= new TextView(this);
TextView tv2 = new TextView(this);
tv.setPadding(5, 0, 5, 0);
tv2.setPadding(5,0,5,0);
String k1 = Integer.toString(k);
tv.setText(k1);
tv2.setText(it_version);
tr.addView(tv);
tr.addView(tv2);
tl.addView(tr,1);
ds.close();
}
} catch (Exception e) {
Log.e("UDP", "Client error", e);
}
If i keep 50 rows am able to display it properly without any time delay, if i put 3000 rows its taking too long time and sometimes app is hanging... I wanna add 50 entries to a table and load the table and again read 50 entries and append to the table without touching any button or anything so i have a table in UI and it will update automatically by reading UDP packets ... how i can achieve that ?? Any clue appreciated.
or once i read the UDP packet i wanna display it on UI[appending to the table],How i can do this ??[Scrolling and all i will take care] please let me know
I already tried using threads but no use
Basically, you need to implement an infinite listview. There are a couple strategies to do this:
You can get all the data and store it in a database and only show the user 50 at a time.
You can fetch only 50 at first and then fetch the next 50 when the user scrolls past them.
You can fetch 100, show 50 and then show next 50 when the user scrolls past the first 50. Pre-fetch the next 100 to show next and so on.
Once you figured out your fetching strategy, you need to implement the actual adapter and listview. Here's a good technique to do this. I would recommend that you don't re-invent the wheel and use this great library called EndlessAdapter unless you want to implement it for learning purposes.
Something like this is what you might use in order to get a infinite list effect when you don't have a cursor.
Please note this is a very rough draft since I deleted the code only relevant to my app, to help for you clarity, and for my privacy and the apps privacy. Also it may not be the best way of doing everything, but it worked the first time I wrote it (which took like 10 minutes) and worked beautifully for a very complex list, so I haven't bothered coming back to it.
class AsyncGetUpdates extends AsyncTask<Void, Void, List<UpdateDTO>>
{
#Override
protected void onPreExecute()
{
showDialog();
super.onPreExecute();
}
#Override
protected List<UpdateDTO> doInBackground(Void... params)
{
return APIHelper.getUpdates(count);
}
#Override
protected void onPostExecute(List<UpdateDTO> result)
{
killDialog();
isCurrentlyUpdating = false;
setAdapterData(result);
super.onPostExecute(result);
}
}
public void setAdapterData(List<UpdateDTO> result)
{
killDialog();
if (this != null && this.getActivity() != null)
{
Log.d(TAG, "setAdapterData");
if (lvUpdatesList.getAdapter() != null)
{
// save index and top position
int index = lvUpdatesList.getFirstVisiblePosition();
View v = lvUpdatesList.getChildAt(0);
int top = (v == null) ? 0 : v.getTop();
updateListAdapter = new UpdateListAdapter(this.getActivity().getLayoutInflater(), result, this);
lvUpdatesList.setAdapter(updateListAdapter);
lvUpdatesList.refreshDrawableState();
lvUpdatesList.setSelectionFromTop(index, top);
}
else
{
updateListAdapter = new UpdateListAdapter(this.getActivity().getLayoutInflater(), result, this);
lvUpdatesList.setAdapter(updateListAdapter);
lvUpdatesList.refreshDrawableState();
}
}
// add in a listener to know when we get to the bottom
lvUpdatesList.setOnScrollListener(new OnScrollListener()
{
#Override
public void onScrollStateChanged(AbsListView view, int scrollState)
{
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)
{
// we do not want to update if we already are
if (isCurrentlyUpdating == false)
{
if (lvUpdatesList.getAdapter() != null && lvUpdatesList.getAdapter().getCount() == count)
{
final int lastItem = firstVisibleItem + visibleItemCount;
if (lastItem == totalItemCount)
{
isCurrentlyUpdating = true;
// add to the count of views we want loaded
count += 20;
// start a update task
new AsyncGetUpdates().execute();
}
}
}
}
});
}
Finally I would like to say that copy pasting might get you the results you want, but it will hinder you future ability. I would say study, read, learn, try, fail, and try again.