2

I am very sorry if I break some rules, or if this has already been asked before. I have used so much time to google examples, and questions on stack overflow and other recourses. But I can simply not understand how I can get a document field from a firestore collection, and show the string value in a jetpack compose text function.

I am a very beginner in programming and Android. So I properly has some fundamental misunderstanding how I should do it but here is my attempt which doesn't work, and I can not understand why.

In Firestore I have a collection, called users with a document called holidaySavings that has a field of type string called name.

I want to show the value of name in a composable text function.

I have a class called storedData that handles Firestore. It has methods for creating and update a collection /document /fields. That works.

But I cant seem to be able to read a field value from a document to a jetpack composable text.

I can read the value from a document field, to the Log in Android studio.

Here is my function in my class where I handle the Firestore database

fun readDataTestFinal(): String{
        val docRef = db.collection("users").document("holidaySavings")
        var returnTest = ""
        docRef.get()
            .addOnSuccessListener { document ->
                if (document != null) {
                    Log.d("Rtest", "DocumentSnapshot data: ${document.data}")

                    // I want to return this so I can use it in a composable text view
                    returnTest = document.get("name").toString()
                } else {
                    Log.d("Rtest", "No such document")
                }
            }
            .addOnFailureListener {  exception ->
                Log.d("Rfail", "get failed with ", exception)
            }
        return returnTest
    }

And here I try to read the value into a jetpack compose Text function.

var newStringFromStoredData by remember {
                mutableStateOf(storedData().readDataTestFinal())
            }
            Text(
                modifier = Modifier.background(color = Color.Blue),
                text = newStringFromStoredData
            )

When I run the app. everything compiles fine, and I get the value from the document field fine, and can see it in the Log in Android Studio.

But the Compose function where call Text with the value newStringFromStoredData it doesn't show on the screen?

Can anyone tell me what it is I don't understand, and how it could be done so I can use the firestore document field and show the value in a jetpack compose Text function?

Jason Aller
  • 3,541
  • 28
  • 38
  • 38
  • There is no way you can do that. Firebase API is asynchronous. So please check the duplicate to see how can you solve this using a callback. You might also be interested in reading this article, [How to read data from Cloud Firestore using get()?](https://medium.com/firebase-tips-tricks/how-to-read-data-from-cloud-firestore-using-get-bf03b6ee4953). – Alex Mamo May 11 '22 at 06:05

2 Answers2

3

The Firebase call is async, which means it will not return the data immediately. This is why the API uses a callback. Therefore, your function readDataTestFinal is always returning an empty string.

One solution you can use is transform your function in a suspend function and call it using a coroutine scope. For example:

suspend fun readDataTestFinal(): String {
    val docRef = firestore.collection("users")
        .document("holidaySavings")
    return suspendCoroutine { continuation ->
        docRef.get()
            .addOnSuccessListener { document ->
                if (document != null) {
                    continuation.resume(document.get("name").toString())
                } else {
                    continuation.resume("No such document")
                }
            }
            .addOnFailureListener { exception ->
                continuation.resumeWithException(exception)
            }
    }
}

In the code above, we are converting a callback call to suspend function.

In your composable, you can do the following:

var newStringFromStoredData by remember {
    mutableStateOf("")
}
Text(newStringFromStoredData)

LaunchedEffect(newStringFromStoredData) {
    newStringFromStoredData =
        try { readDataTestFinal() } catch(e: Exception) { "Error!" }
}

The LaunchedEffect will launch your suspend function and update the result as soon it loads.

A better option would be define this call in a View Model and call this function from it. But I think this answers your question and you can improve your architecture later. You can start from here.

nglauber
  • 18,674
  • 6
  • 70
  • 75
  • Did you actually run this and see if it works, sir? – Richard Onslow Roper May 10 '22 at 18:25
  • Yes. Let me know if you didn't get it. – nglauber May 10 '22 at 18:35
  • 1
    Hello. Thank you a lot for the help. it actually works for me now. I also learned a bit about how coroutines work. – Michael Christensen May 11 '22 at 16:41
  • @MichaelChristensen you should mark the answer as the correct answer if it solved your query. There's a little tick icon just below the votes counter. – Richard Onslow Roper May 11 '22 at 22:21
  • @MARSK Thanks for that. I am new to all this. So any help is much apriciated. I also saw your solution, and I am greatfull for that answer as well. I am currently looking into a a better solution like yours, where there is also a local value when the server is not reached. Just I dont have that much time, since I only do this as a hobby. So when I get more time I will take a deeper look at your solution, and try to figure out how I could use that. – Michael Christensen May 12 '22 at 08:01
  • Good to know the issue was resolved. Happy coding! – Richard Onslow Roper May 12 '22 at 13:19
1

The most convenient, quickest, and apparently the best-in-your-case patch would be to use what are called valueEventListeners.

Firebase provides these helpful methods for you, so that you can keep your app's data up-to-date with the firebase servers.

val docRef = db.collection("cities").document("SF")
docRef.addSnapshotListener { snapshot, e -> // e is for error

    // If error occurs
    if (e != null) {
        Log.w(TAG, "Listen failed.", e)
        return@addSnapshotListener
    }
    
    // If backend value not received, use this to get current local stored value
    val source = if (snapshot != null && snapshot.metadata.hasPendingWrites())
        "Local"
    else
        "Server"
    
    // If request was successful, 
    if (snapshot != null && snapshot.exists()) {
        Log.d(TAG, "$source data: ${snapshot.data}")
        //Update your text variable here
        newStringFromStoredData = snapshot.data // Might need type-conversion
    } else {
        Log.d(TAG, "$source data: null")
    }
}

This will not only solve your problem as described in the question, but will also ensure that whenever the value on the server is changed/updated, your text will update alongside it. It is usually a good best practice to use these listeners, and these are often converted into LiveData objects for respecting the 'separation-of-concerns' principle, but you can use this simple implementation for the simple use-case described.

Another thing, this would usually go in a viewModel, and hence, you should declare you text variable inside the viewmodel too. Try it in the init block.

calss MVVM: ViewModel() {
  init {
    /* Paste Code From Above Here */
  }

  var newStringFromStoredData by mutableStateOf("") 

}

Then read it in the Composable

Text(viewModel.newStringFromStoredData)

Richard Onslow Roper
  • 5,477
  • 2
  • 11
  • 42