What I try to accomplish is the following: I have a collection where documents get added with a timestamp in them. I then want to listen to that collection via snapshotlistener, but just for new documents. So I update my timestamp to the newest document-timestamp received, and try to query only the documents newer than my timestamp. In onCreate I assign lastUpdateTime a date in the past, so that I get the first document added.
val sdf = SimpleDateFormat("dd/MM/yyyy", Locale.US)
try {
lastUpdateTime = sdf.parse("01/01/2000")
} catch (e: ParseException) {
//
}
then I add the snapshotlistener and try to update lastUpdateTime in order to just look for documents newer than thist Date/Time
val path = //...path to my collection
private var lastUpdateTime = Any() //my up-to-date timestamp. I first assign it some date in the past, to make sure it gets the first document added.
// some code
talksListener = path.whereGreaterThan("timestamp", lastUpdateTime)
.addSnapshotListener(EventListener<QuerySnapshot> { snapshot, e ->
if (snapshot != null && !snapshot.isEmpty && !snapshot.metadata.hasPendingWrites()) {
for (dSnapshot in snapshot) {
val thisTimestamp = dSnapshot.get("timestamp")
if (thisTimestamp != null) {
lastUpdateTime = thisTimestamp
}
}
}
})
But every time I add an document, I get the whole collection again.
I also tried all combinations with orderBy and startAt/endAt/startBefore/EndBefore but the result is the same. Either I get nothing, or the whole collection every time a new document is added.
for example:
talksListener = path.orderBy("timestamp").startAfter(lastUpdateTime)
Where is the problem here?
Also, on a different note, is there a possibility to include !snapshot.metadata.hasPendingWrites() into the query in Kotlin. The documentation says to use MetadataChanges.INCLUDE but I do not get how to implement it in Kotlin. Every Hint is much appreciated.
edit 1:
My firestore DB is structured like this:
users/{user}/messages/{message} -> here is the timestamp located
and my path leads to ./messages
edit 2:
the solution is to detach and reattach the listener after the new lastUpdateTime is assigned. That does not sound good to me, so if anyone has a better solution, I am happy to hear it. For the time being I will stick to it though.
Related
I am trying to save a list of data to the Firebase Realtime Database in Kotlin. But when I try to save the order gets shuffled as below.
This is my code. Here the locationlist is the array that contains the list of locations that I want to save in the same order as it is. But when I try to save the after the first element, 10th element is showed. And the pattern continues like that.
ref.child(pk).child("location${count+1}").setValue(locationList).addOnCompleteListener {
Toast.makeText(requireContext() ,"success$pk", Toast.LENGTH_SHORT).show()
}
If you are looking for an ascending order in code, the following lines will keep the order that exist in the array. Assuming you have a property "name" within each object, please try the following code:
val rootRef = FirebaseDatabase.getInstance().reference
val locationRef = rootRef.child("location2")
locationRef.get().addOnCompleteListener { task ->
if (task.isSuccessful) {
for (ds in task.result.children) {
val name = ds.child("name").getValue(String::class.java)
Log.d(TAG, name!!)
}
} else {
Log.d(TAG, task.exception!!.message!!) //Don't ignore potential errors!
}
}
}
If you want to change the order in the Firebase Console, please note that this is not possible. Check for a workaround in my answer from the following post:
How to order the nodes in firebase console based on key
In my firestore database,there are 12+ documents.I am getting the first 3 documents correctly by calling the below function on button click. But on the secondclick, though the documentReference is passed correctly, its not retrieving any data.The querySnapshot size is coming 0. What could be the problem.
Given below is the declaration
private val db: FirebaseFirestore = FirebaseFirestore.getInstance()
private val colRef: CollectionReference = db.collection("Notebook")
private var lastResult: DocumentReference? = null
private lateinit var query: Query
and below is the onButtonClick code :
private fun loadNoteNew() {
#Suppress("SENSELESS_COMPARISON", "LiftReturnOrAssignment")
if (lastResult == null) {
query = colRef.orderBy("priority")
.limit(3)
} else {
Log.i(TAG, "Start ${lastResult!!.id}")
query = colRef.orderBy("priority")
.startAfter(lastResult)
.limit(3)
}
Log.i(TAG, "before get")
query.get()
.addOnSuccessListener { querySnapshot ->
var data = ""
Log.i(TAG, "querySnapshot Size : ${querySnapshot.size()}")
if (lastResult != null) {
Log.i(TAG, "querySnapshot ID : ${lastResult!!.id}")
}
for (snapshot in querySnapshot) {
val note = snapshot.toObject(Note::class.java)
note.id = snapshot.id
val title = note.title
val desc = note.description
val priority = note.priority
data += "${note.id} \nTitle =$title \nDescription = $desc\nPriority : $priority\n\n"
}
if (querySnapshot.size() > 0) {
data += "---------------\n\n"
textView_loadData.append(data)
lastResult = querySnapshot.documents[querySnapshot.size() - 1].reference
Log.i(TAG, lastResult!!.id)
}
}
}
Given below is the logcat for first click
I/FireStoreExample: before get
I/FireStoreExample: querySnapshot Size : 3
I/FireStoreExample: P9hIw4Ai7w4IHP6H3ew3
and given below is the logcat of second click
I/FireStoreExample: Start P9hIw4Ai7w4IHP6H3ew3
I/FireStoreExample: before get
I/FireStoreExample: querySnapshot Size : 0
I/FireStoreExample: querySnapshot ID : P9hIw4Ai7w4IHP6H3ew3
Please help me find out,where i am getting it wrong.
Thanks
The second query result is empty because of a misunderstanding on the semantics of query pagination using startAt and startAfter methods.
Let's say the Notebook collection contains N documents. When you make the first query you're asking for the first 3 documents ordered by the priority field so the query is returning documents 1..3. Then upon the second click you're expecting the query to return the next 3 results so indeed you're expecting documents 4..6. The keypoint here is that both startAt and startAfter paginate based on the value of the ordered field rather than with the last document retrieved. Overall the semantics of startAt and startAfter are roughly as follows.
orderby(X).startAt(Y) => Return documents whose X field is greater than or equal Y
orderby(X).startAfter(Y) => Return documents whose X field is strictly greater than Y
With that in mind, let's examine what the code is actually doing when you make the second query:
// At the end of the first query...
lastResult = querySnapshot.documents[querySnapshot.size() - 1].reference
// Second query
query = colRef.orderBy("priority")
.startAfter(lastResult)
.limit(3)
In the code above you're asking for the documents whose "priority" field is greater than document reference "P9hIw4Ai7w4IHP6H3ew3" and indeed there are no documents greater than that, therefore the result set is empty. Here is api reference for both.
There is yet another thing to note. Because these methods filter upon the fields value the position of the cursor could be ambiguous. For instance, if you have 4 documents with priority 3 and already retrieved the leading three if you set startAfter(3) you'll be missing a document. Similarly, if startAt(3) were to be made you'll get back the same three documents. This is also pointed out in the documentation. All in all you have a couple of options to make this work as intended:
Add another orderby in another field so that documents are uniquely identified by the combination so to prevent any cursor ambiguity and be able to use startAfter with guarantees. Next snippet build upon the doc samples and your code.
// first query
query = colRef.orderBy("priority")
.orderBy("AnotherField")
.limit(3)
// Save last document
lastResult = querySnapshot.documents[querySnapshot.size() - 1]
// Second and next queries
query = colRef.orderBy("priority")
.orderBy("AnotherField")
.startAfter(lastResult)
.limit(3)
Lastly remember that it might be simpler to just query all the documents if they're not many and delay optimizations until they become a performance issue.
I want to add a field of type array inside a collection.
if the field doesn't exist create it. if it exists overwrite it with the new array value.
the field should be called macAddress and it's of type array of String
I have tried the following:
val macInput = setting_mac_text.text.toString()
val macArray = macInput.split(",")
val macList = Arrays.asList(macArray)
val data =
hashMapOf(Pair(FirebaseConstants.USER_MAC_ADDRESS, macArray))
//save it in firebase
db.collection(FirebaseConstants.ORGANIZATION)
.document(orgID + ".${FirebaseConstants.USER_MAC_ADDRESS}")
.set(FieldValue.arrayUnion(macList))
.addOnCompleteListener { task ->
if (task.isSuccessful) {
Log.d(TAG, "successfully inserted")
} else {
Log.d(TAG, " failed ${task.exception}")
}
}
also tried to insert the list itself and hash map like this
val data = hashMapOf(Pair(FirebaseConstants.USER_MAC_ADDRESS, macArray))
db.collection(FirebaseConstants.ORGANIZATION)
.document(orgID)
.set(data))
but it keeps giving me java.lang.IllegalArgumentException: Invalid data. Nested arrays are not supported
what am I doing wrong here?
You're doing three things wrong here:
FieldValue.arrayUnion() is only meant to be used as the value of a field to add elements to that field. The way you are using it now in the first sample, it's being taken as the entire contents of the document.
set() with one parameter is only intended to create or overwrite an entire document. It can't be used to update an existing document. You would have to pass in SetOptions to tell it to merge if you want an update. Or, you would simply use update() to modify an existing document.
Your code that deals with macArray and macList isn't working the way you expect. You are creating a list with one element, which is itself an array. The error message is telling you that you can't have nested arrays like this.
I suggest taking a step back and simplifying your code, removing all the moving parts that don't have to do with Firestore. Just hard code values in your Firestore update until the update works the way you want, then add in the code that works with actual values. Get one simple thing to work, then add to it. If you get an error, you will know that the code you just added was incorrect.
To overwrite an array, you would simply call the set method and have the merge option set to true:
try {
const query = await DatabaseService.queryBuilder({
collection: CollectionName,
});
return await query
.doc(insuranceId)
.set(
{ DOCUMENT_PROPERTY_HERE: ARRAY_HERE },
{ merge: true }
);
} catch (exception) {
return Promise.reject(exception);
}
I am trying to use Android Room described there: https://codelabs.developers.google.com/codelabs/android-room-with-a-view-kotlin/#13
source codes: https://github.com/googlecodelabs/android-room-with-a-view/tree/kotlin/app/src/main/java/com/example/android/roomwordssample
I would like to choose one record when all records will be loaded.
I tried in the end of "onCreate(savedInstanceState: Bundle?)" function add this code:
var rnd : Random = Random(System.currentTimeMillis())
var words : List<Word>? = wordViewModel.allWords.value
// now wordViewModel.allWords.value is NULL
if (words != null) {
var textViewRandomWord: TextView = findViewById<TextView>(R.id.textView)
textViewRandomWord.text = words.get(rnd.nextInt(words.size)).word
}
But in this time is variable wordViewModel.allWords.value is still null.
Can you help me, where to add this code (or any similar code) to get random record from all saved records and show it when activity is created?
Thank you.
wordViewModel.allWords is [LiveData] : https://developer.android.com/topic/libraries/architecture/livedata object, so its good to get value by observer
wordViewModel.allWords.observe(this, Observer { words ->
// Do your implementation
})
I'm trying to set the epoch when data is created in Firestore. I'm looking to get some similar result to what is done in the real-time database, using ServerValue.TIMESTAMP.
I don't want to set it by using the device time System.getCurrentMillis because that time can be changed by the user.
According to docs an update needs to be done, the problem with that is the format. This is my code:
Map<String, Object> map = new HashMap<>();
map.put("timestamp", FieldValue.serverTimestamp());
reference.update(map);
And this is the result in the Firebase web console:
I was very surprised it is in spanish, which could be useful in some situations but epoch is what I'm chasing. Try to see the bright side and stick with it and thought that I was seeing the web in spanish, so I changed the language in the footer selector, it didn't change. On this point I'm assuming is set in the project language.
Back to the epoch attempt. Considering my project is using the real-time database as well, I try to set it in that way:
Map<String, Object> map = new HashMap<>();
map.put("timestamp", ServerValue.TIMESTAMP);
reference.update(map);
It did upload something, but it was just nonsense.
I think using epoch as the server-side timestamp is a better standard approach, after that every client can transform it to the user convenience and locale.
Can the epoch by set as server value in Firestore?
UPDATE
The answer marked as correct lead me to some interesting findings that I would like to share, so others in the same situation can benefit from:
There is no need to set the epoch because of the FieldValue.serverTimestamp() it is a date object handled by the database, what we see in the console is just a friendly way to show it.
Since FieldValue.serverTimestamp() is a date object it can be sort as any other timestamp could be, if you add orderBy("timestamp", Query.Direction.DESCENDING) to your query (or Query.Direction.ASCENDING) it will sort the results correctly.
And regarding to a the #34m0 comment, that is right, clients should not take care of the logic for setting the creation time, but it should be done in Functions.
The object that results from setting a Firestore field with FieldValue.serverTimestamp() is an instance of java.util.Date. When you later read the value, you can get the epoch time using getTime().
As an example, for a document created like this:
Map<String, Object> doc = new HashMap<>();
doc.put("timestamp", FieldValue.serverTimestamp());
The resulting value can be read like this:
docRef.get().addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>() {
#Override
public void onComplete(#NonNull Task<DocumentSnapshot> task) {
if (task.isSuccessful()) {
DocumentSnapshot snapshot = task.getResult();
if (snapshot != null) {
Map<String,Object> map = snapshot.getData();
Date date = (Date) map.get("timestamp");
Log.d(TAG, "date=" + date);
Log.d(TAG, "time=" + date.getTime());
} else {
Log.d(TAG, "No such document");
}
} else {
Log.d(TAG, "get() failed with ", task.getException());
}
}
});