2023-07-23

How to update Android UI on Firestore result?

I am trying to write a database query in Cloud Firestore in Kotlin.

The request itself is working but I am having some problems with the async workflow.

My goal is to make a database request that fetches a lot of data and puts this into an object. Once this is complete for all objects, the normal workflow of the app should continue. (because I need to display some of this data in my UI).

My database structure: I have a collection of users -> Each user has a collection of their own recipes. -> In each recipe there is a collection of Steps to complete and a collection of ingredients

My goal is to get the data and show it on my app screen (code below), but with my current code, the app starts the request and continues to show the app screen (which means there is no data loaded up to this point).

Is there a way to make my database request more efficient (as I have 3 requests per recipe) and also to make it work async? So that when the data gets loaded the UI will recognize a change and show this to the UI later on or the UI waits till all the requests are complete?

Sorry if this is a basic question about coroutines, but I am pretty new to this topic and could not find any info in the Firebase documentation.

My Code:

in My Main Activity:

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        loadRecipes()//should load the information from the Database
        initUiElements()//should show the Information in a Recycler View
    }

-> load Recipies calls the following Class with the loadloadRecipes() function

Class: FirebaseRecipes:

fun loadRecipes(){

        //First DB Request to get the Recipe Names and Durations
        Log.d("FirebaseRecipes", "loadRecipes()")
        db.collection("users").document(userID.toString()).collection("Recipes").get()
            .addOnSuccessListener { result ->
                for (document in result) {
                    val duration = document.data["duration"].toString()
                    val name = document.data["name"].toString()
                    var recipe = Recipe(name, duration)
                    RecipesList.add(recipe)

                    //Second DB Request to get the Ingredients
                    db.collection("users").document(userID.toString()).collection("Recipes").document(document.id).collection("ingredients").get()
                        .addOnSuccessListener { result ->
                            for (document in result) {
                                val amount = document.data["amount"].toString()
                                val name = document.data["name"].toString()
                                val barcode = document.data["unit"].toString()
                                val ingredient = Ingredient(name, amount, barcode)
                                recipe.ingredients.add(ingredient)
                            }
                        }

                    //Third DB Request to get the Steps
                 db.collection("users").document(userID.toString()).collection("Recipes").document(document.id).collection("steps").get()
                        .addOnSuccessListener { result ->
                            for (document in result) {
                                val step = document.data["text"].toString()
                                recipe.steps.add(step)
                            }
                        }
                }
            }
    }

Solution:

with the answers of @AlexMamo, @UsmanMahmood and @Nguyá»…nMinhKhoa I was able to get the following solution to my problem:

I Created a ViewModel that contains my List:

private val _recipeList =
        MutableLiveData<CustomResponse<List<Recipe>>>(CustomResponse.Loading())
    val recipeList: LiveData<CustomResponse<List<Recipe>>>
        get() = _recipeList

    fun shareRecipe(recipes: List<Recipe>) {
        _recipeList.value = CustomResponse.Success(recipes)
    }

in the class that should retrieve the data from Firebase I created the following function, to retrieve the data and store it to the View Model:

fun loadRecipes(activity: Activity) {
        val model =
            ViewModelProvider(activity as ViewModelStoreOwner)[ViewModel::class.java]

        db.collection("users").document(userID.toString()).collection("Recipes").get()
            .addOnSuccessListener { result ->
                val recipesList = ArrayList<Recipe>()
                for (document in result) {
                    val duration = document.data["duration"].toString()
                    val name = document.data["name"].toString()
                    var recipe = Recipe(name, duration)
                    recipesList.add(recipe)

                    recipe.steps = document.data["steps"] as ArrayList<String>

                    val ingredients =
                        document.data["ingredients"] as ArrayList<HashMap<String, Any>>
                    for (ingredient in ingredients) {
                        val name = ingredient["name"].toString()
                        val amount = (ingredient["amount"] as Long).toInt()
                        val ingredient = Ingredient(name, amount)
                        recipe.ingredients.add(ingredient)
                    }
                    model.shareRecipe(recipesList)
                }
            }
    }

A Custom Response:

sealed class CustomResponse<T>(val  data: T? = null, val errorMessage:String? =null) {                                    ;
    class Loading<T> : CustomResponse<T>()
    class Success<T>(data: T? = null) : CustomResponse<T>(data = data)
    class Error<T>(errorMessage: String) : CustomResponse<T>(errorMessage = errorMessage)
}

and in the fragments I can call it like this:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val model = ViewModelProvider(requireActivity())[RecipesViewModel::class.java]
model.recipeList.observe(viewLifecycleOwner) { response ->
            when (response as CustomResponse<List<Recipe>>) {
                is CustomResponse.Loading -> {
                    rvRecipes.visibility = View.INVISIBLE
                    progressBar.visibility = View.VISIBLE
                }
                is CustomResponse.Success -> {
                    rvRecipes.visibility = View.VISIBLE
                    progressBar.visibility = View.GONE

                    Log.d("FirebaseRecipes", "Recipes loaded within Fragment")

                    adapter.content = response.data as ArrayList<Recipe>
                    adapter.notifyDataSetChanged()
                }
                is CustomResponse.Error -> {
                    response.errorMessage?.let {
                        rvRecipes.visibility = View.INVISIBLE
                        //TODO: Display error message
                    }
                }
            }
        }


No comments:

Post a Comment