Kotlin MVVM Firestore - android

How i can remove this carsListener?
Otherwise I get an infinite download
override fun getUserData(userId: String): Flow<Response<User>> = callbackFlow {
Response.Loading
val userClients = arrayListOf<Client>()
val clientsListener = firestore
.collection("users").document(userId).collection("clients")
.addSnapshotListener { clients, e ->
if (clients != null) {
for (client in clients) {
val carsList = arrayListOf<Car>()
val carsListener = firestore
.collection("users").document(userId)
.collection("clients").document(client.id)
.collection("cars")
.addSnapshotListener { cars, error ->
if (cars != null) {
for (car in cars) {
val carsData = car.toObject(Car::class.java)
carsList.add(carsData)
}
}
}
val data = client.toObject(Client::class.java)
userClients.add(data.copy(cars = carsList))
}
} else
Response.Error(e?.message ?: e.toString())
}
val snapShotListener = firestore.collection("users").document(userId)
.addSnapshotListener { user, error ->
val response = if (user != null) {
val userData = user.toObject(User::class.java)
Response.Success<User>(userData?.copy(clients = userClients)!!)
} else
Response.Error(error?.message ?: error.toString())
trySend(response).isSuccess
}
awaitClose {
clientsListener.remove()
snapShotListener.remove()
}
}
I tried everything, but it didn't give any result
if you know how to do it differently, please tell me

My understanding is that you have nested collections: Users > Clients > Cars. In this case, you don't need to add Snapshot Listeners to all those collections. You can simply attach a listener to Users and then call get() to get the data from its subcollections.
Also, starting in version 24.4.0 of the Cloud Firestore Android SDK (BoM 31.0.0) you can take advantage of:
the snapshots() function which returns a Flow<QuerySnapshot> so that you don't have to write your own implementation of CallbackFlow.
the await() function which allows you to await for the result of a CollectionReference.get().
Be sure to update to the latest Firebase version on your app/build.gradle file to be able to use these functions.
And putting it all together should result in a code that looks like this:
override fun getUserData(userId: String): Flow<Response<User>> {
val userRef = firestore.collection("users").document(userId)
val clientsRef = userRef.collection("clients")
return userRef.snapshots().map { documentSnapshot ->
// Convert the DocumentSnapshot to User class
val fetchedUser = documentSnapshot.toObject<User>()!!
// Fetch the clients for that User
val clientsSnapshot = clientsRef.get().await()
// iterate through each client to fetch their cars
val clients = clientsSnapshot.documents.map { clientDoc ->
// Convert the fetched client
val client = clientDoc.toObject<Client>()!!
// Fetch the cars for that client
val carsSnapshot = clientsRef.document(clientDoc.id)
.collection("cars").get().await()
// Convert the fetched cars
val cars = carsSnapshot.toObjects<Car>()
client.copy(cars = cars)
}
Response.Success(fetchedUser.copy(clients = clients))
}.catch { error ->
Response.Error<User>(error.message ?: error.toString())
}
}
As a side note: Reading from all these 3 collections at once can become expensive, depending on the number of Documents that each collection contains. That's due to how Firestore pricing works - you get charged for each document read.
Consider adjusting your database structure to help save costs. Here are some resources that could be helpful:
Cloud Firestore Data Model
Structure Data
Understand Cloud Firestore Billing
Cloud Firestore Pricing | Get to know Cloud Firestore #3
Example Cloud Firestore costs

Related

how is this example of realm object listener to live data work while my assumption, onActive was called only once right?

i'm currently reading this live realm object documentation , part that i dont i understand is this part
override fun onActive() {
super.onActive()
val obj = value
if (obj != null && RealmObject.isValid(obj)) {
RealmObject.addChangeListener(obj, listener)
}
}
class CounterModel : ViewModel() {
private var realm: Realm? = null
private val _counter: LiveRealmObject<Counter> = LiveRealmObject(null)
val counter: LiveData<Counter>
get() = _counter
init {
val appID = "YOUR APP ID HERE" // TODO: replace this with your App ID
// 1. connect to the MongoDB Realm app backend
val app = App(
AppConfiguration.Builder(appID)
.build()
)
// 2. authenticate a user
app.loginAsync(Credentials.anonymous()) {
if(it.isSuccess) {
Log.v("QUICKSTART", "Successfully logged in anonymously.")
// 3. connect to a realm with Realm Sync
val user: User? = app.currentUser()
val partitionValue = "example partition"
val config = SyncConfiguration.Builder(user!!, partitionValue)
// because this application only reads/writes small amounts of data, it's OK to read/write from the UI thread
.allowWritesOnUiThread(true)
.allowQueriesOnUiThread(true)
.build()
// open the realm
realm = Realm.getInstance(config)
// 4. Query the realm for a Counter, creating a new Counter if one doesn't already exist
// access all counters stored in this realm
val counterQuery = realm!!.where<Counter>()
val counters = counterQuery.findAll()
// if we haven't created the one counter for this app before (as on first launch), create it now
if (counters.size == 0) {
realm?.executeTransaction { transactionRealm ->
val counter = Counter()
transactionRealm.insert(counter)
}
}
// 5. Instantiate a LiveRealmObject using the Counter and store it in a member variable
// the counters query is life, so we can just grab the 0th index to get a guaranteed counter
this._counter.postValue(counters[0]!!)
} else {
Log.e("QUICKSTART", "Failed to log in anonymously. Error: ${it.error.message}")
}
}
}
fun incrementCounter() {
realm?.executeTransaction {
counter.value?.let { counterValue ->
counterValue.add()
_counter.postValue(counterValue)
Log.v("QUICKSTART", "Incremented counter. New value: ${counterValue.value.get()}")
}
}
}
override fun onCleared() {
realm?.close()
}
}
source
on the viewmodel it was instantiate with null value, so onActive will called with value = null so no listener to attached but how on earth will it eventually attach listener to value when object is not null and also valid
i tried to implement this with my own realm model, did some logging but mine was only called once for the onActive method and the value was null because i was first initialize with null, i was expecting that when i update the object on foreground service it listened and and set the value and then attach the listener to the obj somehow

Saving and deleting data from firestore async [duplicate]

I have created an app with Kotlin and Firebase Firestore. Now I need to implement coroutines as there is so much work on the main thread. But I'm also a beginner so it's something new to me. I've watched some tutorials on this but I didn't find complete tutorials on Firestore with coroutines. So I need some help to implement coroutines in my app In such parts like these (I tried by myself but didn't get it).
Retrieving posts from Firestore.
private fun retrievePosts() {
FirebaseFirestore.getInstance().collection("Posts")
.orderBy("timeStamp", Query.Direction.DESCENDING)
.get()
.addOnSuccessListener { queryDocumentSnapshots ->
postList?.clear()
for (documentSnapshot in queryDocumentSnapshots) {
val post = documentSnapshot.toObject(Post::class.java)
postList?.add(post)
}
postAdapter?.notifyDataSetChanged()
postAdapter?.setOnPostClickListener(this)
if (isRefreshed) {
swipe_refresh_home?.setRefreshing(false)
isRefreshed = false
}
swipe_refresh_home?.visibility = VISIBLE
progress_bar_home?.visibility = GONE
}.addOnFailureListener { e ->
Log.d(TAG, "UserAdapter-retrieveUsers: ", e)
swipe_refresh_home?.visibility = VISIBLE
progress_bar_home?.visibility = GONE
}
}
Getting user data into an adapter
private fun userInfo( fullName: TextView, profileImage: CircleImageView,
about: TextView, uid: String,
userLocation: TextView, itemRoot: LinearLayout ) {
val userRef = FirebaseFirestore.getInstance().collection("Users").document(uid)
userRef.get()
.addOnSuccessListener {
if (it != null && it.exists()) {
val user = it.toObject(User::class.java)
Glide.with(mContext).load(user?.getImage()).placeholder(R.drawable.default_pro_pic).into(profileImage)
fullName.text = user?.getFullName().toString()
about.text = user?.getAbout()
if (user?.getLocation() != ""){
userLocation.visibility = VISIBLE
userLocation.text = user?.getLocation()
}
if (profileImage.drawable == null){
itemRoot.visibility = GONE
}
else{
itemRoot.visibility = VISIBLE
}
}
}
}
And this Save post button in an adapter.
private fun savedPost(postId: String, saveButton: ImageView?) {
FirebaseFirestore.getInstance().collection("Users").document(currentUserID)
.collection("Saved Posts").document(postId)
.get()
.addOnSuccessListener {
if (it.exists()) {
saveButton?.setImageResource(drawable.ic_bookmark)
} else {
saveButton?.setImageResource(drawable.bookmark_post_ic)
}
}
}
As I see your code, you are using the following query:
val queryPostsByTimestamp = FirebaseFirestore.getInstance().collection("Posts")
.orderBy("timeStamp", Query.Direction.DESCENDING)
Most probably to get a list of Post objects from your "Posts" collection.
In order to use Kotlin Coroutines, don't forget to add the following dependencies in the Gradle (app) file:
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.3.9"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"
I'll provide you a solution using the MVVM architecture pattern. So we'll use a repository class and a ViewModel class. For the asynchronous calls to Firestore, we'll use Flow.
For the response that we get from the database call, we need a sealed class that looks like this:
sealed class Response<out T> {
class Loading<out T>: Response<T>()
data class Success<out T>(
val data: T
): Response<T>()
data class Failure<out T>(
val errorMessage: String
): Response<T>()
}
Assuming that you have a "Post" class, let's create in the repository class the following function:
fun getPostsFromFirestore() = flow {
emit(Loading())
emit(Success(queryPostsByTimestamp.get().await().documents.mapNotNull { doc ->
doc.toObject(Post::class.java)
}))
}. catch { error ->
error.message?.let { errorMessage ->
emit(Failure(errorMessage))
}
}
So we'll emit an object according to the state. When first-time calling the function, we emit a loading state using emit(Loading(), when we get the data we emit the List<Post> and if we get an error, we emit the error message using Failure(errorMessage).
Now we need to call this function, from the ViewModel class:
fun getPosts() = liveData(Dispatchers.IO) {
repository.getPostsFromFirestore().collect { response ->
emit(response)
}
}
With the above function, we collect the data that we get from the getPostsFromFirestore() function call, and we emit the result further as a LiveData object so it can be observed in the activity/fragment like this:
private fun getPosts() {
viewModel.getPosts().observe(this, { response ->
when(response) {
is Loading -> //Load a ProgessBar
is Success -> {
val postList = response.data
//Do what you need to do with your list
//Hide the ProgessBar
}
is Failure -> {
print(response.errorMessage)
//Hide the ProgessBar
}
}
})
}
That's pretty much of it!
I don't know Firebase, so I may miss something, but generally speaking you don't need a special support in the library to use it with coroutines. If you start a background coroutine and then execute your above code in it, then Firebase will probably run within your coroutine without any problems.
The only problematic part could be listeners. Some libs invoke callbacks in the thread that was used to execute them, but some dispatch callbacks to a specific thread. In the case of Firebase it seems by default it runs listeners in the main thread. If this is not what you want, you can pass an executor to run callbacks within coroutines as well, e.g.:
.addOnSuccessListener(Dispatchers.Default.asExecutor()) { ... }

FireStore not return all results

I want to get data from firestore, pretend that I have 10 documents in a collection. after that I must get data inside every document, so I save data in ArrayList. But FireBase never return all documents in a collection. Sometimes it returns only 5 ,6 docs in collection that has 10 docs.
my fireBaseUtil :
fun getDocumentByQueryAList( idQuery: List<String>, callBack: (ArrayList<DocumentSnapshot>) -> Unit) {
val listDocumentSnapshot = ArrayList<DocumentSnapshot>()
collRef = fireStore.collection("myCollection")
val size = idQuery.size
for (i in 0 until size) {
val query = collRef.whereEqualTo("fieldQuery", idQuery[i])
query.get().addOnSuccessListener { documents ->
for (document in documents) {
listDocumentSnapshot.add(document)
if (i == size - 1) {
callBack.invoke(listDocumentSnapshot)
}
}
}
}
}
I log out when size = 10 , but i = 8 it called invoke....
UserRepository:
FireBaseUtil.getDocumentByQueryAList{
// myList.addAll(listGettedByCallback)
}
->> when I want to have data in my list I call FireBaseUtil.getDocumentByQueryAList. I know firebase return value async but I dont know how to get all my doc then receiver.invoke("callbackValue").
Please tell me is there any solution. Thank in advance.
The problem you are experiencing is that you are expecting the queries to be run in order like so:
get idQuery[0], then add to list, then
get idQuery[1], then add to list, then
get idQuery[2], then add to list, then
...
get idQuery[8], then add to list, then
get idQuery[9], then add to list, then
invoke callback
But in reality, all of the following things happen in parallel.
get idQuery[0] (add to list when finished)
get idQuery[1] (add to list when finished)
get idQuery[2] (add to list when finished)
...
get idQuery[8] (add to list when finished)
get idQuery[9] (add to list and invoke callback when finished)
If the get idQuery[9] finishes before some of the others, you will be invoking your callback before the list is completely filled.
A primitive way to fix this would be to count the number of finished get queries, and when all of them finish, then invoke the callback.
fun getDocumentByQueryAList( idQuery: List<String>, callBack: (ArrayList<DocumentSnapshot>) -> Unit) {
val listDocumentSnapshot = ArrayList<DocumentSnapshot>()
collRef = fireStore.collection("myCollection")
val size = idQuery.size
val finishedCount = 0
for (i in 0 until size) {
val query = collRef.whereEqualTo("fieldQuery", idQuery[i])
query.get().addOnSuccessListener { documents ->
for (document in documents) {
listDocumentSnapshot.add(document)
}
if (++finishedCount == size) { // ++finishedCount will add 1 to finishedCount, and return the new value
// all tasks done
callBack.invoke(listDocumentSnapshot)
}
}
}
}
However, this will run into issues where the callback is never invoked if any of the queries fail. You could use a addOnFailureListener or addOnCompleteListener to handle these failed tasks.
The more correct and proper way to do what you are expecting is to make use of Tasks.whenAll, which is used in a similar fashion to how you see JavaScript answers using Promise.all. I'm still new to Kotlin syntax myself, so expect the following block to potentially throw errors.
fun getDocumentByQueryAList( idQueryList: List<String>, callBack: (ArrayList<DocumentSnapshot>) -> Unit) {
val listDocumentSnapshot = ArrayList<DocumentSnapshot>()
collRef = fireStore.collection("myCollection")
val getTasks = new ArrayList<Task<Void>>();
for (idQuery in idQueryList) {
val query = collRef.whereEqualTo("fieldQuery", idQuery)
getTasks.add(
query.get()
.onSuccessTask { documents ->
// if query succeeds, add matching documents to list
for (document in documents) {
listDocumentSnapshot.add(document)
}
}
)
}
Tasks.whenAll(getTasks)
.addOnSuccessListener { results ->
callback.invoke(listDocumentSnapshot)
}
.addOnFailureListener { errors ->
// one or more get queries failed
// do something
}
}
Instead of using the callback, you could return a Task instead, where the last bit would be:
return Tasks.whenAll(getTasks)
.onSuccessTask { results ->
return listDocumentSnapshot
}
This would allow you to use the following along with other Task and Tasks methods.
getDocumentByQueryAList(idQueryList)
.addOnSuccessListener { /* ... */ }
.addOnFailureListener { /* ... */ }
Use smth like this using RxJava:
override fun getAllDocs(): Single<List<MyClass>> {
return Single.create { emitter ->
db.collection("myCollection").get()
.addOnSuccessListener { documents ->
val list = mutableListOf<MyClass>()
documents.forEach { document ->
list.add(mapDocumentToMyClass(document))}
emitter.onSuccess(list)
}
.addOnFailureListener { exception ->
emitter.onError(exception)
}
}
}
private fun mapDocumentToMyClass(documentSnapshot: QueryDocumentSnapshot) =
MyClass(
documentSnapshot.get(ID).toString(),
documentSnapshot.get(SMTH).toString(),
// some extra fields
null
)
Or smth like this using coroutine:
override suspend fun getAllDocs(): List<MyClass> {
return try {
val snapshot =
db.collection("myCollection")
.get()
.await()
snapshot.map {
mapDocumentToMyClass(it)
}
} catch (e: Exception) {
throw e
}
}
This functions helps you to get all data from one doc

livedata coroutine does not observe the data

I use MVVM architecture in my project. When it first called, it works. but after that, it does not work.
here is my code.
fun loadYgosuData() : LiveData<ArrayList<YgosuData>> {
Log.d(TAG, "loadYgosuData()...")
return liveData {
Log.d(TAG, "liveData Scope running....")
val element = withContext(Dispatchers.IO) {
Jsoup.connect("https://www.ygosu.com/community/real_article")
.get()
.select("div.board_wrap tbody tr")
}
emit(setYgosuData(element))
}
}
fun setYgosuData(tmpDoc: Elements?) : ArrayList<YgosuData> {
var tmpList = ArrayList<YgosuData>()
if (tmpDoc != null) {
//Log.d(TAG, "element : $tmpDoc")
for (e in tmpDoc) {
var title = e.select("td.tit a")
var name = e.select("td.name a")
var read = e.select("td.read")
var date = e.select("td.date")
var url = e.select("td.tit a").attr("href").toString()
var tmpYgosuData = YgosuData(
title = title.text(),
name = name.text(),
read = read.text(),
date = date.text(),
url = url
)
tmpList.add(tmpYgosuData)
}
}
Log.d(TAG, "여기2 ${tmpList.toString()}")
return tmpList
}
Could you help me please?
Thank you.
From official doc
The liveData building block serves as a structured concurrency primitive between coroutines and LiveData. The code block starts executing when LiveData becomes active and is automatically canceled after a configurable timeout when the LiveData becomes inactive. If it is canceled before completion, it is restarted if the LiveData becomes active again. If it completed successfully in a previous run, it doesn't restart. Note that it is restarted only if canceled automatically. If the block is canceled for any other reason (e.g. throwing a CancelationException), it is not restarted.
That's why it works for the first time as you mentioned.
Solution1: combine liveData with Transformations
private val body: MutableLiveData<String> = MutableLiveData()
val YgousData = body.switchMap {
liveData {
Log.d(TAG, "liveData Scope running....")
val element = withContext(Dispatchers.IO) {
Jsoup.connect("https://www.ygosu.com/community/real_article")
.get()
.select("div.board_wrap tbody tr")
}
emit(setYgosuData(element))
}
}
fun callApi(param:String){
body.value = param
}
You can change the body based on which you want to trigger api call
Solution2: You can do api call explicitly and then post the data to livedata (w/o using livedata builder)

RxJava: How to extract same observeOn from different functions?

Basically my Android app have some meta data which need to report to backend server in different scenarios:
data class SearchMetaData(
val searchId: String?,
val isRateLimit: Boolean
)
In order to make the code clean, I make the minimal case as follows. All the report logic are similar, each of the function subscribe the metadata provider and get the value that need to report.
fun logEvent1() {
fetchMetaData().observeOn(schedulers.mainThread()).subscribe({ metadata ->
...//lots of other events data here
val sessionMetadata = SessionMetadata()
sessionMetadata.id = metadata.searchId
sessionMetadata.limiit = metadata.isRateLimit
event1.session = sessionMetadata
...
report(event1)
})
}
fun logEvent2() {
fetchMetaData().observeOn(schedulers.mainThread()).subscribe({ metadata ->
...//lots of other events data here
val sessionMetadata = SessionMetadata()
sessionMetadata.id = metadata.searchId
sessionMetadata.limiit = metadata.isRateLimit
event2.session = sessionMetadata
...
report(event2)
})
}
fun logEvent3() {
fetchMetaData().observeOn(schedulers.mainThread()).subscribe({ metadata ->
...//lots of other events data here
val sessionMetadata = SessionMetadata()
sessionMetadata.id = metadata.searchId
sessionMetadata.limiit = metadata.isRateLimit
event3.session = sessionMetadata
...
report(event3)
})
}
My concern is every time we change the schema of metadata, we need to update all the logEventX , I was wondering if maybe we could extract all the subscribe in different functions and get the metadata?
Consider an extension function using compose and doOnSuccess
Single<MetaData>.handleLogging() : Single<MetaData>{
return compose{
it.doOnSuccess{ metaData ->
val sessionMetadata = SessionMetadata()
sessionMetadata.id = metadata.searchId
sessionMetadata.limiit = metadata.isRateLimit
report(sessionMetaData)
}
}
}
//usage
fetchMetaData().handleLogging().subscribe{
//other uncommon logic here.
}

Categories

Resources