Setting global variable with Cloud Firestore on Kotlin - android

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.

Related

Managing Data With Coroutines

In my android project I have tried to implement a shared View Model which does all the reading and writing data. I do this by using Mutable Live Data and my activity calls an update function within the View Model to update the Live Data. However I can't figure out how to get the data after it has been accessed. It seems that I am trying to update my UI before the data gets accessed. I have looked up this problem and it seems the solution has something to do with coroutines. I have not been successful implementing coroutines and I always get a null value for my data.
ViewModel :
private val firebaseDatabase: DatabaseReference = FirebaseDatabase.getInstance().reference
private val fAuth = FirebaseAuth.getInstance()
private val user: FirebaseUser = fAuth.currentUser!!
private var _saveLocation: MutableLiveData<LocationEvent> = MutableLiveData<LocationEvent>()
val saveLocation: LiveData<LocationEvent> get() = _saveLocation
fun loadData() {
firebaseDatabase.child("User").child(user.uid).child("SaveLocation").get()
.addOnSuccessListener {
_saveLocation.value = LocationEvent(
it.child("title").getValue<String>()!!,
it.child("organizer").getValue<String>()!!,
LatLng(
it.child("locationLatLng").child("latitude").value as Double,
it.child("locationLatLng").child("longitude").value as Double
),
it.child("address").getValue<String>()!!,
it.child("description").value as String,
it.child("allDay").value as Boolean,
it.child("sdate").getValue<Calendar>()!!,
it.child("edate").getValue<Calendar>()!!,
it.child("notifications").getValue<MutableList<Int>>()!!,
user.uid
)
}.addOnFailureListener {}
}
Activity function :
private fun loadSaveData() {
dataViewModel.loadData()
//using log statement just to see if any value
//Always get null
Log.d("MainFragment", "${dataViewModel.saveLocation.value}")
}
I did not include any attempt at coroutines above.
Question
How can I use coroutines to fix this problem?
If not coroutines than what?
(Side Question) : Why does casting to type Calendar cause a crash?
Any help whether its a solution or pointing me to a solution would be much appreciated.
Whenever you use code with names like "add listener" or "set listener" or ones with words like "fetch" or "async" in the name and take lambda parameters, you are calling an asynchronous function. This means the function returns before it finishes (and usually before it even starts) doing what you requested it to.
The purpose of the listener/callback/lambda function you pass to it is to do something sometime in the future, whenever the work eventually is completed. It could only be a few milliseconds in the future, but it absolutely will not happen until after your other code under the function call is complete.
In this case, your get() call to Firebase is synchronous, and you are adding a listener to it to tell it what to do with the results, when they eventually arrive. Then your flow of code continues on synchronously. Back in your loadSaveData() function, you are checking for the results, but the request and your listener have not been completed yet.
You don't need coroutines to get around this. Coroutines are a convenient syntax for dealing with code that normally uses callbacks, but regardless of whether you use coroutines, you need to understand what is going on. IO operations like what you're using cannot be done on the main thread, which is why they are done synchronously.
There's a lot more info about this in this StackOverflow question.

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()?

How to stop a Flowable from emitting more items

I've got a question about Flowables. I already have a few solutions for this issue, but I would like to double-check if these are the best possible solutions or not.
Context
I have an Interactor that is supposed to bookmark recipes on the DB. It looks like this:
/**
* This Interactor marks a recipe as "bookmarked" on the DB. The Interactor actually switches
* the isBookmarked value of the related recipeId. If it was marked as true, it switches its value
* to false. If it was false, then it switches its value to true.
*/
class BookmarkRecipeInteractorImpl(
private val recipesCacheRepository: RecipesCacheRepository
) : BookmarkRecipeInteractor {
override fun execute(recipeId: Int, callback: BookmarkRecipeInteractor.Callback) {
// Fetches the recipe from DB. The getRecipeById(recipeId) function returns a Flowable.
// Internally, within the RecipesCacheRepository, I'm using room.
recipesCacheRepository.getRecipeById(recipeId).flatMap { originalRecipe ->
// Switches the isBookmarked value
val updatedRecipe = originalRecipe.copy(
isBookmarked = !originalRecipe.isBookmarked
)
// Update the DB
recipesCacheRepository.updateRecipe(updatedRecipe)
// Here's the issue, since I'm updating a DB record and the getRecipeById returns
// a Flowable, as soon as I update the DB, the getRecipeById is going to get triggered
// again, and switch the value again, and again, and again...
}
.subscribe(
{
callback.onSuccessfullyBookmarkedRecipe(it.response)
},
{
callback.onErrorFetchingRecipes()
}
)
}
}
So, if you follow the code, the error is pretty straightforward. I get stuck on a loop, where I constantly change the recipe record.
Possible solutions
1) Have two different functions on my DAO, one called getRecipeByIdFlowable(id) that returns a Flowable, and another called getRecipeByIdSingle(id) that returns a rx.Single. That way I can expose the getRecipeByIdSingle(id) through the Repository and use it instead of the function that returns the Flowable. That way I cut the loop.
Pro: It works.
Con: I don't like having functions like this on my DAO.
2) Save the Disposable on a lateinit property and dispose it as soon as the subscriber triggers the onNext().
Pro: It works.
Con: I don't like having to do something like this, feels hacky.
3) Using ...getRecipeById(recipeId).take(1).flatMap... so it only handles the first emitted object.
Pro: It works, it looks tidy.
Con: I'm not sure if there's a better way to do it.
Question
Ideally, I would like to call some function that just allows me to disable the Flowable behavior and prevent it from emitting more items if the DB changes. So far the solution that I like the most is #3, but I'm not really sure if this is the right way to do it.
Thanks!
Edit 1
I'm just adding a bit more of information about the use case here. I need an Interactor that given a recipeId changes the isBookmarked value on DB to its oposite.
The DB records look like:
data class DbRecipeDto(
#PrimaryKey
val id: Int,
val name: String,
val ingredients: List<String>,
val isBookmarked: Boolean = false
)
I know that maybe there's some other ways in which I could tackle this issue differently. Maybe I could pass the recipeId arg and a bookmark (Boolean) argument and just run the update query.
But this use case it is totally made up, just an example; The thing that I'm trying to figure out how to prevent a Flowable from emitting more items if something changes on the DB.
You should probably call .take(1).singleOrError() on the end of getRecipeById(recipeId).
This will take the first item (or the error) emitted by the Flowable retrieved by calling getRecipeById and wrap it in a Single. In my opinion this correctly matches the semantics of what you want to achieve.
In addition, if I recall correctly, because you will be subscribing on a Single by doing this, your Flowable will not continue to do work after the first item is consumed by the downstream call to singleOrError.

Firebase Database "equalTo" without ordering

Let's say I have this object in db
{
value:60
status:PLAYING // could be PLAYING or FINISHED for simplicity
}
and I want to listen this object, or want to be notified when status becomes FINISHED
Currently following is not working
myRef.child("status").equalTo("PLAYING").addValueEventLisener...
it just triggers onDataChange regardless of status and returns snapshot value as null
On the other hand, if I omit PLAYING and use as following:
myRef.child("status").addValueEventLisener...
onDataChange is triggered as usual and snapshot value is not null
I have tried orderByChild it is not helping or I am doing things wrong.
That is an orderByValue() query:
myRef.child("status").orderByValue().equalTo("PLAYING")

Check if data exist when using onChildEventListener

Is it possible to check if child data exists over a Firebase DataReference when using observeChildEvent? I'm using onChildEventListener to fill a RecyclerView on my application but during the data change, I show a ProgressBar. The problem appears when there's no data in the current query, so I never receive an event and I can't hide my progressBar.
Is there any way to achieve this without launch a previous observeSingleValueEvent with a limit(1) value to check in the dataSnapshot if there is available data?
I'm using RxFirebase2 to work with Firebase, so I did a method to check if a DatabaseReference have childrens, using it together with RxJava to avoid use OnChildEvents when my reference have no childrens:
public Single<Boolean> checkIfRefHaveAvailableChild(DatabaseReference databaseReference){
final Query query = databaseReference.limitToFirst(MIN_RETRIEVE_DATA);
return RxFirebaseDatabase.observeSingleValueEvent(query)
.subscribeOn(Schedulers.io())
.take(1)
.map(dataSnapshot -> dataSnapshot.hasChildren())
.single(false);
}

Categories

Resources