I am new to Firestore and I am developing an android app where I am loading comments in Recycler View.
Below is sample data class for comment.
data class Comment(
val id: String,
val text: String,
val user: DocumentReference
)
Currently I am using this below code in onBindViewHolder in adapter
comment.userId.get()
.addOnCompleteListener {
if (it.isSuccessful) {
val u = it.result.toObject(User::class.java)
user = u.name
binding.userTv.text = user
}
}
binding.commentTv.text = comment.text
I have to explicitally run Task<TResult> every time to fetch the user in adapter.
I am looking for a better way to retrieve user from comment to display username is a single query.
Typically when dealing with usernames that may update regularly, traditionally you would store the raw user name string in each document which requires heavy read/writes to keep it updated. Instead, it has been found better to store them in a master document/collection that assigns all user UID's with the display name as the value, allowing you to look up any user UID and know it from one source of truth.
This has been made easier with the new Bundle Feature which allows all apps to preload documents in your app to prevent the need to fetch them every time. The only catch is you will need to retrieve the data when a new user isn't in their cached data yet or have a dedicated source for all new users to reduce overhead in those situations.
Source: Data Bundles
Related
So im trying to understand how data is saved on android. I am currently working on a simple notes app. How it currently works, is the user creates a new note, types whatever they want, and once they hit the back button that entry is saved in to a list. I am using compose viewmodel so the entries are saved up until the point the app is destroyed. what is the best way to save the list entries so they are pemenantly saved on the phone. This is also just a general question on how apps generally save user input data that is not saved in the cloud.
here is some example of the code.
data class NotesBlueprint(
val header: String,
val note: String,
val key: Int
)
data class NotesVars(
val header:String = "", <<<< instance of header and the note
val note: String = "" <<<<
var list: List<NotesBlueprint> = mutableListOf() <<< list that the Notes are saved in to
)
You can store the data on android via Shared Preference, Database, and Files. This storage overview guide gives the idea regarding different types of the storage system and how they are saved on android.
In your case, if you want to save notes which user has written, it would be better to go with the database.
Android provides the Room library for storing data in the local database. You can check out the guide here.
I am trying to push the form data to the firebase-firestore. And I also did it successfully. But, the problem is that whenever I am trying to submit the form data again and again it just updates the last data with the current data.
Actually, my requirement is that whenever the user hit the submit button. It creates a document with a random id and stores the all data into that specific id that is generated.
You are specifying the document ID in .document() so it'll overwrite the same document. If you want a document with a random ID on every click, try using add() instead as shown below:
val collectionRef = FirebaseFirestore.getInstance().collection("Maintainance")
collectionRef.add(user).addOnCompleteListener(...)
Alternatively, you can leave .document() empty to get a DocumentReference with a random ID:
val userDocument = FirebaseFirestore.getInstance().collection("Maintanance").document() // <-- don't pass an ID
In addition to #Dharmaraj answer:
CASE_1: In a case where you need to track each user's all submitted forms, probably from your explanation you may need to organize each user's form.
Therefore if you need to organize each user's form then create another sub-collection [example: document(userId).collection("USER_FORMS")] within userID document like this:
val documentRef = FirebaseFirestore.getInstance().collection("Maintainance").document(UserUtils.user?.id.toString()).collection("USER_FROMS").document();
CASE_2 : In a case where you need to make your own custom document ID:
1- make a random number or string or any other data type.
2- The random number/string variable must be local to the code block/method that will execute the form submision function.
3- use the number/string generated as the form document Id like this:
//This must be local so as user clicks submision button so as it generates new random number;
val randomFormId = "generateThenumberOrStringAndInitializeTheVariable";
Then use the random number as the form document Id like this:
val documentRef = FirebaseFirestore.getInstance().collection("Maintainance").document(UserUtils.user?.id.toString()).collection("USER_FROMS").document(randomFormId);
At this moment we are able to retrieve the city ID given the name of it. But I'm trying to reduce the cost of the billing. And taking a look to the code we are doing like this:
var token = AutocompleteSessionToken.newInstance();
Places.initialize(context, <API_KEY>)
var placesClient = Places.createClient(context)
val request = FindAutocompletePredictionsRequest.builder()
.setLocationBias(RectangularBounds.newInstance(UtilsPlaces.generateGlobalWorldBounds()))
.setTypeFilter(TypeFilter.CITIES)
.setSessionToken(token)
.setQuery(<CITY_NAME>)
.build()
placesClient.findAutocompletePredictions(request).addOnSuccessListener { response ->
var prediction = response.autocompletePredictions[0]
placeAddress.placeId = prediction.placeId
<Do stuff with the place id retrieved>
}
.addOnFailureListener {
<Do stuff in case of failure>
}
So my question is.. is the good way to retrieve the id? Or exist a better way?
I saw we can get information (BASIC SKU is what we need) doing fetchPlace() but If I try to do like this:
val placeFields = asList(Place.Field.ID)
val fetchPlaceRequest = FetchPlaceRequest.newInstance(<CITY_NAME>, placeFields)
placesClient.fetchPlace(fetchPlaceRequest)
.addOnSuccessListener { response ->
<Do stuff with the place id retrieved>
}
.addOnFailureListener {
<Do stuff in case of failure>
}
But all the time falls in the Failure listener. I suppose I can't fetch the place using the city name instead of the ID.
Not too sure about the sdk approach as i haven't used it, i have used the REST api apporach previously to do this
https://maps.googleapis.com/maps/api/place/autocomplete/json?input=&key=
Reducing cost will be tricky. Here's what i have resorted to in past projects to reduce this cost as best as i could:
1) the query will take place once every 2 characters are typed (you can change 2 to 3 or 4 or whatever is best for your project)
2) Save the results ie: place id and query in the local sqlite db. Before every actual autocomplete query, check if there is a matching result in the local db, if so, show those, else if no matching results are saved, then and only then perform the query.
A combination of these two should reduce your costs quite a bit as per my experience. Unfortunately i am only aware of these two solutions right now
I am creating an app where users can upload photos and tag them, and then tags that they have uses are added to the list of that user's interests.
I am making a transition from Realtime database to cloud firestore and having a bit of an issue.
I want to store all these tags in an array inside a document for each user. Whenever a user uploads a photo, these tags should be added to this array.
I am using the update method as I don't want the list to be overwritten, but the data doesn't seem to be created at all. Could it be because for a new user the list doesn't actually exist?
I need the function to create the document and array if it is the first time the user adds any interests, or just update the list and add items if the array already exists.
Also, the tags are added to a complete list of all the tags across the app. For this one I've tried to use the set method but then the list just gets completely overwritten, even when I added SetOptions.merge()
val db = FirebaseFirestore.getInstance()
val userInterestsDoc = db.collection("interests").document(currentUser.uid)
val allTagsListDoc = db.collection("all_tags").document("all_tags")
for (tag in imageTagsList) {
userInterestsDoc.update("interests_list", FieldValue.arrayUnion(tag))
.addOnSuccessListener {
allTagsListDoc.set(
mapOf(
"all_tags_list" to arrayListOf(tag)
), SetOptions.merge()
)
}
I've read the Firebase docs on Stucturing Data. Data storage is cheap, but the user's time is not. We should optimize for get operations, and write in multiple places.
So then I might store a list node and a list-index node, with some duplicated data between the two, at very least the list name.
I'm using ES6 and promises in my javascript app to handle the async flow, mainly of fetching a ref key from firebase after the first data push.
let addIndexPromise = new Promise( (resolve, reject) => {
let newRef = ref.child('list-index').push(newItem);
resolve( newRef.key()); // ignore reject() for brevity
});
addIndexPromise.then( key => {
ref.child('list').child(key).set(newItem);
});
How do I make sure the data stays in sync in all places, knowing my app runs only on the client?
For sanity check, I set a setTimeout in my promise and shut my browser before it resolved, and indeed my database was no longer consistent, with an extra index saved without a corresponding list.
Any advice?
Great question. I know of three approaches to this, which I'll list below.
I'll take a slightly different example for this, mostly because it allows me to use more concrete terms in the explanation.
Say we have a chat application, where we store two entities: messages and users. In the screen where we show the messages, we also show the name of the user. So to minimize the number of reads, we store the name of the user with each chat message too.
users
so:209103
name: "Frank van Puffelen"
location: "San Francisco, CA"
questionCount: 12
so:3648524
name: "legolandbridge"
location: "London, Prague, Barcelona"
questionCount: 4
messages
-Jabhsay3487
message: "How to write denormalized data in Firebase"
user: so:3648524
username: "legolandbridge"
-Jabhsay3591
message: "Great question."
user: so:209103
username: "Frank van Puffelen"
-Jabhsay3595
message: "I know of three approaches, which I'll list below."
user: so:209103
username: "Frank van Puffelen"
So we store the primary copy of the user's profile in the users node. In the message we store the uid (so:209103 and so:3648524) so that we can look up the user. But we also store the user's name in the messages, so that we don't have to look this up for each user when we want to display a list of messages.
So now what happens when I go to the Profile page on the chat service and change my name from "Frank van Puffelen" to just "puf".
Transactional update
Performing a transactional update is the one that probably pops to mind of most developers initially. We always want the username in messages to match the name in the corresponding profile.
Using multipath writes (added on 20150925)
Since Firebase 2.3 (for JavaScript) and 2.4 (for Android and iOS), you can achieve atomic updates quite easily by using a single multi-path update:
function renameUser(ref, uid, name) {
var updates = {}; // all paths to be updated and their new values
updates['users/'+uid+'/name'] = name;
var query = ref.child('messages').orderByChild('user').equalTo(uid);
query.once('value', function(snapshot) {
snapshot.forEach(function(messageSnapshot) {
updates['messages/'+messageSnapshot.key()+'/username'] = name;
})
ref.update(updates);
});
}
This will send a single update command to Firebase that updates the user's name in their profile and in each message.
Previous atomic approach
So when the user change's the name in their profile:
var ref = new Firebase('https://mychat.firebaseio.com/');
var uid = "so:209103";
var nameInProfileRef = ref.child('users').child(uid).child('name');
nameInProfileRef.transaction(function(currentName) {
return "puf";
}, function(error, committed, snapshot) {
if (error) {
console.log('Transaction failed abnormally!', error);
} else if (!committed) {
console.log('Transaction aborted by our code.');
} else {
console.log('Name updated in profile, now update it in the messages');
var query = ref.child('messages').orderByChild('user').equalTo(uid);
query.on('child_added', function(messageSnapshot) {
messageSnapshot.ref().update({ username: "puf" });
});
}
console.log("Wilma's data: ", snapshot.val());
}, false /* don't apply the change locally */);
Pretty involved and the astute reader will notice that I cheat in the handling of the messages. First cheat is that I never call off for the listener, but I also don't use a transaction.
If we want to securely do this type of operation from the client, we'd need:
security rules that ensure the names in both places match. But the rules need to allow enough flexibility for them to temporarily be different while we're changing the name. So this turns into a pretty painful two-phase commit scheme.
change all username fields for messages by so:209103 to null (some magic value)
change the name of user so:209103 to 'puf'
change the username in every message by so:209103 that is null to puf.
that query requires an and of two conditions, which Firebase queries don't support. So we'll end up with an extra property uid_plus_name (with value so:209103_puf) that we can query on.
client-side code that handles all these transitions transactionally.
This type of approach makes my head hurt. And usually that means that I'm doing something wrong. But even if it's the right approach, with a head that hurts I'm way more likely to make coding mistakes. So I prefer to look for a simpler solution.
Eventual consistency
Update (20150925): Firebase released a feature to allow atomic writes to multiple paths. This works similar to approach below, but with a single command. See the updated section above to read how this works.
The second approach depends on splitting the user action ("I want to change my name to 'puf'") from the implications of that action ("We need to update the name in profile so:209103 and in every message that has user = so:209103).
I'd handle the rename in a script that we run on a server. The main method would be something like this:
function renameUser(ref, uid, name) {
ref.child('users').child(uid).update({ name: name });
var query = ref.child('messages').orderByChild('user').equalTo(uid);
query.once('value', function(snapshot) {
snapshot.forEach(function(messageSnapshot) {
messageSnapshot.update({ username: name });
})
});
}
Once again I take a few shortcuts here, such as using once('value' (which is in general a bad idea for optimal performance with Firebase). But overall the approach is simpler, at the cost of not having all data completely updated at the same time. But eventually the messages will all be updated to match the new value.
Not caring
The third approach is the simplest of all: in many cases you don't really have to update the duplicated data at all. In the example we've used here, you could say that each message recorded the name as I used it at that time. I didn't change my name until just now, so it makes sense that older messages show the name I used at that time. This applies in many cases where the secondary data is transactional in nature. It doesn't apply everywhere of course, but where it applies "not caring" is the simplest approach of all.
Summary
While the above are just broad descriptions of how you could solve this problem and they are definitely not complete, I find that each time I need to fan out duplicate data it comes back to one of these basic approaches.
To add to Franks great reply, I implemented the eventual consistency approach with a set of Firebase Cloud Functions. The functions get triggered whenever a primary value (eg. users name) gets changed, and then propagate the changes to the denormalized fields.
It is not as fast as a transaction, but for many cases it does not need to be.