How to upload documents to Firestore - android

There was a question about loading data from Firebase Firestore Cloud.
There is a simple query:
val firestore = Firebase.firestore
suspend fun getDocuments(timestamp: Timestamp): List<Entity> =
firestore
.collection("collection")
.orderBy("modified_at", Query.Direction.ASCENDING)
.whereGreaterThan("modified_at", timestamp)
.get()
.await()
.toObjects(Entity::class.java)
What happens if something goes wrong while uploading documents? Will I receive incomplete data or an error?
In what order are the documents loaded? Does orderBy affect the load order?

What happens if something goes wrong while uploading documents?
As far as I can see, you aren't uploading any documents, you're only reading them using get() which:
Executes the query and returns the results as a QuerySnapshot.
When it comes to performing CRUD operations in Firestore, there is always the possibility that an operation can fail, for example, due to improper security rules. That being said, it's always recommended to handle errors. So your method should contain a try-catch as sown below:
suspend fun getDocuments(timestamp: Timestamp): List<Entity> {
return try {
firestore
.collection("collection")
.orderBy("modified_at", Query.Direction.ASCENDING)
.whereGreaterThan("modified_at", timestamp)
.get()
.await()
.toObjects(Entity::class.java)
} catch (e: Exception) {
throw e
}
}
However, a more elegant solution would be to use a class that contains two fields, the data, and the exception. This practice is explained in the following resource:
How to read data from Cloud Firestore using get()?
In what order are the documents loaded?
You get the documents ordered according to what you pass as an argument to the orderBy() method.
Does orderBy affect the load order?
For sure, that's why we are using it, to have a particular order.

Related

What counts as a read in Firestore?

What counts as a read in Firestore:
db.collection("Collection").document(id).get()
OR:
db.collection("Collection").document(id).get().addOnCompleteListener { task ->
task.result.get("field")
task.result.get("field")
task.result.get("field")
task.result.get("field")
}
Both. You are calling .get() method so that makes a call to Firestore servers (if offline persistence is not enabled or document is not found in cache). Just creating the DocumentReference however does not charge:
// just a reference, document data not fetched
db.collection("Collection").document(id)
In the 2nd code snippet, the task.result seems to be fetched data and you are just read a single field locally.

How to properly get the results from firestore database fetches embracing ascynchronous functionality? - Android - Kotlin - MVVM

Because database fetches usually happen asynchronously by default, a variable that holds the data from the firebase database fetch will be null when used right after the fetch. To solve this I have seen people use the ".await()" feature in Kotlin coroutines but this goes against the purpose of asynchronous database queries. People also call the succeeding code from within 'addOnSuccessListener{}' but this seems to go against the purpose of MVVM, since 'addOnSuccessListener{}' will be called in the model part of MVVM, and the succeeding code that uses the fetched data will be in the ViewModel. The answer I'm looking for is maybe a listener or observer that is activated when the variable (whose value is filled from the fetched data) is given a value.
Edit:
by "succeeding code" I mean what happens after the database fetch using the fetched data.
As #FrankvanPuffelen already mentioned in his comment, that's what the listener does. When the operation for reading the data completes the listener fires. That means you know if you got the data or the operation was rejected by the Firebase servers due to improper security rules.
To solve this I have seen people use the ".await()" feature in Kotlin coroutines but this goes against the purpose of asynchronous database queries.
It doesn't. Using ".await()" is indeed an asynchronous programming technique that can help us prevent our applications from blocking. When it comes to the MVVM architecture pattern, the operation for reading the data should be done in the repository class. Since reading the data is an asynchronous operation, we need to create a suspend function. Assuming that we want to read documents that exist in a collection called "products", the following function is needed:
suspend fun getProductsFirestore(): List<Product> {
var products = listOf<Product>()
try {
products = productsRef.get().await().documents.mapNotNull { snapShot ->
snapShot.toObject(Product::class.java)
}
} catch (e: Exception) {
Log.d("TAG", e.message!!)
}
return products
}
This method can be called from within the ViewModel class:
val productsLiveData = liveData(Dispatchers.IO) {
emit(repository.getProductsFromFirestore())
}
So it can be observed in activity/fragment class:
private fun getProducts() {
viewModel.producsLiveData.observe(this, {
print(it)
//Do what you need to do with the product list
})
}
I have even written an article in which I have explained four ways in which you can read the data from Cloud Firestore:
How to read data from Cloud Firestore using get()?

Cloud Functions in Firebase: call parameter to be type Document Snapshot

I am trying to use cloud functions to get a subcollection from a starting point. My cloud function is written in typescript and goes by:
export async function getCollection(path: any, start:any, orderBy: any) {
return admin
.firestore()
.collection(path)
.orderBy(orderBy)
.startAfter(start)
.limit(10)
.get()
.then(snapshot => snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })));
}
My code in Kotlin to call for the cloud function from my android app is:
fun getReleasedAnimalsData(startAfter: DocumentSnapshot):Task<ReleasedData>{
/*val gsonObject = Gson().toJson(startAfter)
val jsonObject = JSONObject(gsonObject)*/
val data = hashMapOf("startAt" to startAfter)
return functions
.getHttpsCallable("function")
.call(data)
.continueWith { task ->
val json = Gson().toJson(task.result?.data)
val returnedData= Gson().fromJson(json,ReleasedData::class.java)
returnedData
}
}
When I try to do the calling, I get the following error:
Object cannot be encoded in JSON: DocumentSnapshot
I understand that Document Snapshot is a complex object and cant be serialized just like that but I cant seem to find the way to call the startAfter property from Firebase without a document snapshot.
You might be wondering: Why is he not making the call from the Android Firebase SDK? That is because this task I am trying to do is part of a bigger algorithm and I would prefer to only do one call to the instead of many calls from the android app. Is there a way to make this possible? Or should I just give up and use my Coroutines to do more than one async call?
Any help is appreciated! :)
If you can't pass a DocumentSnapshot object to startAfter, you can just pass the values of the fields that are used in the orderBy part of the query. See the API documentation for startAfter. It takes field values OR a document snapshot. And:
The order of the provided values must match the order of the order by clauses of the query.
So if you have a single orderBy on field X, then you can pass a single value for field X of the last document you saw.

Setting global variable with Cloud Firestore on Kotlin

again :) I got a question about Cloud Firestore and Kotlin.
I need to get data from firestore with some code like this:
{
val comments = mutableListOf<Comment>()
val firestore = FirebaseFirestore.getInstance()
firestore.collection(collection).document(documentID).collection("comments")
.addSnapshotListener { querySnapshot, firebaseFirestoreException ->
comments = querySnapshot?.toObjects(Comment::class.java)!!
// do something with 'comments'. Works: comments is populated
}
// do something with variable 'comments'. Doesn't work: comments is now empty
}
The variable 'comments' gets populated inside the listener curly brackets but when the listener ends, the value goes back to 0.
I've researched online and found examples in JAVA that works perfectly this way, for example:
https://youtu.be/691K6NPp2Y8?t=246
My purpose is to fetch data only ONCE from the Cloud Firestore and store that value in a global variable, comments.
Please, let me know if you have a solution for this.
Thank you.
The value doesn't "go back to zero". You should understand that the database query is asynchronous, and addSnapshotListener returns immediately, before the query completes. The final value is only known when the listener is invoked some time later.
Also, you should know that if you just want to query a single time, you should use get() instead of addSnapshotListener(). It is also asynchronous and returns immediately, and the Task it returns will get invoked some time later. There are no synchronous options that block the caller until the query is complete - you will need to learn how to do your work asynchronously.

Kotlin Coroutines Flow with Room and state handling

I'm trying out the new coroutine's flow, my goal is to make a simple repository that can fetch data from a web api and save it to db, also return a flow from the db.
I'm using room and firebase as the web api, now everything seems pretty straight forward until i try to pass errors coming from the api to the ui.
Since i get a flow from the database which only contains the data and no state, what is the correct approach to give it a state (like loading, content, error) by combining it with the web api result?
Some of the code i wrote:
The DAO:
#Query("SELECT * FROM users")
fun getUsers(): Flow<List<UserPojo>>
The Repository:
val users: Flow<List<UserPojo>> = userDao.getUsers()
The Api call:
override fun downloadUsers(filters: UserListFilters, onResult: (result: FailableWrapper<MutableList<UserApiPojo>>) -> Unit) {
val data = Gson().toJson(filters)
functions.getHttpsCallable("users").call(data).addOnSuccessListener {
try {
val type = object : TypeToken<List<UserApiPojo>>() {}.type
val users = Gson().fromJson<List<UserApiPojo>>(it.data.toString(), type)
onResult.invoke(FailableWrapper(users.toMutableList(), null))
} catch (e: java.lang.Exception) {
onResult.invoke(FailableWrapper(null, "Error parsing data"))
}
}.addOnFailureListener {
onResult(FailableWrapper(null, it.localizedMessage))
}
}
I hope the question is clear enough
Thanks for the help
Edit: Since the question wasn't clear i'll try to clarify. My issue is that with the default flow emitted by room you only have the data, so if i were to subscribe to the flow i would only receive the data (eg. In this case i would only receive a list of users). What i need to achieve is some way to notify the state of the app, like loading or error. At the moment the only way i can think of is a "response" object that contains the state, but i can't seem to find a way to implement it.
Something like:
fun getUsers(): Flow<Lce<List<UserPojo>>>{
emit(Loading())
downloadFromApi()
if(downloadSuccessful)
return flowFromDatabase
else
emit(Error(throwable))
}
But the obvious issue i'm running into is that the flow from the database is of type Flow<List<UserPojo>>, i don't know how to "enrich it" with the state editing the flow, without losing the subscription from the database and without running a new network call every time the db is updated (by doing it in a map transformation).
Hope it's clearer
I believe this is more of an architecture question, but let me try to answer some of your questions first.
My issue is that with the default flow emitted by room you only have
the data, so if i were to subscribe to the flow i would only receive
the data
If there is an error with the Flow returned by Room, you can handle it via catch()
What i need to achieve is some way to notify the state of the app,
like loading or error.
I agree with you that having a State object is a good approach. In my mind, it is the ViewModel's responsibility to present the State object to the View. This State object should have a way to expose errors.
At the moment the only way i can think of is a "response" object that
contains the state, but i can't seem to find a way to implement it.
I have found that it is easier to have the State object that the ViewModel controls be responsible for errors instead of an object that bubbles up from the Service layer.
Now with these questions out of the way, let me try to propose one particular "solution" to your issue.
As you mention, it is common practice to have a Repository that handles retrieving data from multiple data sources. In this case, the Repository would take the DAO and an object that represents getting data from the network, let's call it Api. I am assuming that you are using FirebaseFirestore, so the class and method signature would look something like this:
class Api(private val firestore: FirebaseFirestore) {
fun getUsers() : Flow<List<UserApiPojo>
}
Now the question becomes how to turn a callback based API into a Flow. Luckily, we can use callbackFlow() for this. Then Api becomes:
class Api(private val firestore: FirebaseFirestore) {
fun getUsers() : Flow<List<UserApiPojo> = callbackFlow {
val data = Gson().toJson(filters)
functions.getHttpsCallable("users").call(data).addOnSuccessListener {
try {
val type = object : TypeToken<List<UserApiPojo>>() {}.type
val users = Gson().fromJson<List<UserApiPojo>>(it.data.toString(), type)
offer(users.toMutableList())
} catch (e: java.lang.Exception) {
cancel(CancellationException("API Error", e))
}
}.addOnFailureListener {
cancel(CancellationException("Failure", e))
}
}
}
As you can see, callbackFlow allows us to cancel the flow when something goes wrong and have someone donwnstream handle the error.
Moving to the Repository we would now like to do something like:
val users: Flow<List<User>> = Flow.concat(userDao.getUsers().toUsers(), api.getUsers().toUsers()).first()
There are a few caveats here. first() and concat() are operators you will have to come up with it seems. I did not see a version of first() that returns a Flow; it is a terminal operator (Rx used to have a version of first() that returned an Observable, Dan Lew uses it in this post). Flow.concat() does not seem to exist either. The goal of users is to return a Flow that emits the first value emitted by any of the source Flows. Also, note that I am mapping DAO users and Api users to a common User object.
We can now talk about the ViewModel. As I said before, the ViewModel should have something that holds State. This State should represent data, errors and loading states. One way that can be accomplished is with a data class.
data class State(val users: List<User>, val loading: Boolean, val serverError: Boolean)
Since we have access to the Repository the ViewModel can look like:
val state = repo.users.map {users -> State(users, false, false)}.catch {emit(State(emptyList(), false, true)}
Please keep in mind that this is a rough explanation to point you in a direction, there are many ways to accomplish state management and this is by no means a complete implementation. It may not even make sense to turn the API call into a Flow, for example.
The answer from Emmanuel is really close to answering what i need, i need some clarifications about some of it.
It may not even make sense to turn the API call into a Flow
You are totally right, in fact i only want to actually make it a coroutine, i don't really need it to be a flow.
If there is an error with the Flow returned by Room, you can handle it via catch()
Yes i discovered this after posting the question. But my problem is more something like:
I'd like to call a method, say "getData", this method should return the flow from db, start the network call to update the db (so that i'm going to be notified when it's done via the db flow) and somewhere in here, i would need to let the ui know if db or network errored, right?. Or should i maybe do a separate "getDbFlow" and "updateData" and get the errors separately for each one?
val users: Flow> = Flow.concat(userDao.getUsers().toUsers(), api.getUsers().toUsers()).first()
This is a good idea, but i'd like to keep the db as the single source of truth, and never return to the ui any data directly from the network

Categories

Resources