skip to Main Content

I have a ViewModel that accesses the database (Firebase) and counts documents. The structure of the database is this: Collection "Posts" –> Document –> Collection "Likes" –> Documents.
So I access the collection "Posts" and then for each document that contains my username, I access the collection "Likes" and get the snapshot size to count how many likes are there. The problem is that when a new Like document is created the ViewModel won’t get the updates until the app is restarted.

The way I am using the ViewModel:
I have a navigation graph and that’s where I create an instance of the ViewModel, then I call the function that retrieves the data and then I pass this instance as an argument in the other screen where I need the data. In the other screen, I access the variable where the data is stored. The variable where data is stored in a StateFlow (because I pass here the counter value not the documents)

Code (ViewModel):

class PointsViewModel : ViewModel() {
    private val firebase = Firebase.firestore
    private var _points = MutableStateFlow(0)
    val points: StateFlow<Int> = _points

    fun calculate(username: String){
        firebase.collection("Posts")
            .whereEqualTo("PostUser",username)
            .get()
            .addOnSuccessListener { posts ->
                for(post in posts){
                    post.reference.collection("Likes").get().addOnSuccessListener { likes ->
                        _points.value += likes.size()
                    }
                }
            }
    }
}

Code (Where I create instance in NavGraph):

val pointsViewModel: PointsViewModel = viewModel()

//sharedViewModel contains the username
sharedViewModel.user?.username?.let { pointsViewModel.calculate(it) }

Code (Where I need the data):

fun AwardsScreen(sharedViewModel: SharedViewModel,pointsViewModel: PointsViewModel){
    
    val points by pointsViewModel.points.collectAsState()
    

    Column(modifier = Modifier
        .fillMaxWidth()
        .padding(15.dp, 15.dp)) {


        Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
           Text(text = points.toString())
        }
    }

I tried to do all these operations inside the screen I need the data. So I tried to access the database and retrieve all data from there, adding a snapshotListener to get the updates but it didn’t work

2

Answers


  1. First, just a small nitpick, the _points can be val and actually should be since you do not want to change the actual instance, just its value.

    The other thing I noticed is that you are using the += operand which might be the culprit here. Try assigning a new value like _points.value = _points.value + likes.size().

    The docs say

    For the assignment operations, for example a += b, the compiler
    performs the following steps:

    If the function from the right column is available:

    If the corresponding binary function (that means plus() for
    plusAssign()) is available too, a is a mutable variable, and the
    return type of plus is a subtype of the type of a, report an error
    (ambiguity).

    Make sure its return type is Unit, and report an error otherwise.

    Generate code for a.plusAssign(b).

    Otherwise, try to generate code for a = a + b (this includes a type
    check: the type of a + b must be a subtype of a).

    It might need a completely new value without referencing itself in the assignment.

    And in the end, I would recommend using a state holder. So you would have a class like this:

    data class StateHolder(val points: Int = 0)
    

    And you would use it in the viewModel like so:

    class PointsViewModel : ViewModel() {
        private val firebase = Firebase.firestore
        private val _stateHolder = MutableStateFlow(StateHolder())
        val stateHolder: StateFlow<StateHolder> = _stateHolder
    
        fun calculate(username: String){
            firebase.collection("Posts")
                .whereEqualTo("PostUser",username)
                .get()
                .addOnSuccessListener { posts ->
                    for(post in posts){
                        post.reference.collection("Likes")
                            .get()
                            .addOnSuccessListener { likes ->
                                _stateHolder.value = stateHolder
                                    .value
                                    .copy(points =  
                                        stateHolder.value.points + likes.size())
                        }
                    }
                }
        }
    }
    

    And finally, to observe it in a composable:

    val stateHolder by pointsViewModel.stateHolder.collectAsState()
        
    
        Column(modifier = Modifier
            .fillMaxWidth()
            .padding(15.dp, 15.dp)) {
    
    
            Row(modifier = Modifier.fillMaxWidth(), 
                horizontalArrangement = Arrangement.Center) {
               Text(text = "${stateHolder.points}")
            }
        }
    
    Login or Signup to reply.
  2. This is not how you should do when it comes to counters in Firestore. When you count documents as you do now, it will cost you one read for each document your query returns, and this can be expensive when it comes to a larger number of likes. Unfortunately, count() doesn’t provide real-time updates. However, it will be less expensive if you create and maintain a counter yourself. That can simply be a field of type number that exists in a document. This means that each time a new document is added to a collection you can simply increment the counter, and decrement it when a document is deleted. On this document, you can attach a real-time listener and you’ll have a counter that is always up to date. This means that in order to display the number of likes, you’ll only need to pay for a single read.

    Such operations should not be performed in the client-side code but in a trusted environment. So I recommend you write a function in Cloud Functions for Firebase that does that automatically for you.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search