如何在Firestore结果上更新Android用户界面?

huangapple go评论65阅读模式
英文:

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 Recipes 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
                    }
                }
            }
        }

注意:由于你要求只返回翻译好的部分,因此我只提供了翻译部分,没有包括代码和解决方案的具体细节。如果你需要更多信息或具体解释,请随时提出。

英文:

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(&quot;FirebaseRecipes&quot;, &quot;loadRecipes()&quot;)
        db.collection(&quot;users&quot;).document(userID.toString()).collection(&quot;Recipes&quot;).get()
            .addOnSuccessListener { result -&gt;
                for (document in result) {
                    val duration = document.data[&quot;duration&quot;].toString()
                    val name = document.data[&quot;name&quot;].toString()
                    var recipe = Recipe(name, duration)
                    RecipesList.add(recipe)

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

                    //Third DB Request to get the Steps
                 db.collection(&quot;users&quot;).document(userID.toString()).collection(&quot;Recipes&quot;).document(document.id).collection(&quot;steps&quot;).get()
                        .addOnSuccessListener { result -&gt;
                            for (document in result) {
                                val step = document.data[&quot;text&quot;].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&lt;CustomResponse&lt;List&lt;Recipe&gt;&gt;&gt;(CustomResponse.Loading())
    val recipeList: LiveData&lt;CustomResponse&lt;List&lt;Recipe&gt;&gt;&gt;
        get() = _recipeList

    fun shareRecipe(recipes: List&lt;Recipe&gt;) {
        _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(&quot;users&quot;).document(userID.toString()).collection(&quot;Recipes&quot;).get()
            .addOnSuccessListener { result -&gt;
                val recipesList = ArrayList&lt;Recipe&gt;()
                for (document in result) {
                    val duration = document.data[&quot;duration&quot;].toString()
                    val name = document.data[&quot;name&quot;].toString()
                    var recipe = Recipe(name, duration)
                    recipesList.add(recipe)

                    recipe.steps = document.data[&quot;steps&quot;] as ArrayList&lt;String&gt;

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

A Custom Response:

sealed class CustomResponse&lt;T&gt;(val  data: T? = null, val errorMessage:String? =null) {                                    ;
    class Loading&lt;T&gt; : CustomResponse&lt;T&gt;()
    class Success&lt;T&gt;(data: T? = null) : CustomResponse&lt;T&gt;(data = data)
    class Error&lt;T&gt;(errorMessage: String) : CustomResponse&lt;T&gt;(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 -&gt;
            when (response as CustomResponse&lt;List&lt;Recipe&gt;&gt;) {
                is CustomResponse.Loading -&gt; {
                    rvRecipes.visibility = View.INVISIBLE
                    progressBar.visibility = View.VISIBLE
                }
                is CustomResponse.Success -&gt; {
                    rvRecipes.visibility = View.VISIBLE
                    progressBar.visibility = View.GONE

                    Log.d(&quot;FirebaseRecipes&quot;, &quot;Recipes loaded within Fragment&quot;)

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

答案1

得分: 0

在Firebase中,集合中的基本限制似乎是100个项目。

在Firestore中没有这样的限制。您可以在一个集合中添加任意数量的文档,甚至是数十亿。

我想要一个用户的所有食谱。

因此,如果您认为用户最多可以拥有的食谱数量是100,则可以将它们全部添加到单个文档中,包括配料和步骤。这样,您只需支付一次读取操作的费用。但是,在单个文档中存储数据有一定的限制。您只能存储最多1 MiB的数据。

如果您认为可以突破这个限制,尽管我表示怀疑,那么您可以考虑创建两个,甚至三个文档,而不需要为每个食谱、配料和步骤创建单独的文档。所有数据都可以存储为数组中的对象。

英文:

> The base limit in Firebase seems to be 100 items within a collection

There is no such limitation in Firestore. You can add as many documents as you want in a collection, even billions.

> I want all the recipes for one user.

So if you think that the maximum number of recipes a user can have is 100, then you can add all of them in a single document, including the ingredients and the steps. In this way, you'll have to pay for a single-read operation. However, there is a limitation when it comes to how much data you can store in a single document. You can only store data up to 1 MiB.

If you think that you can get over this limitation, which I doubt, then instead of creating one document, you might consider creating two, or even three. So there is no need in this case to create a separate document for each recipe, ingredient, and step. All the data cand be stored as object in an array.

答案2

得分: 0

你可以使用 LiveData 来实现,并使用自定义响应来管理成功、加载和错误状态。当数据正在加载时,在 UI 中显示加载器,你需要像这样做:

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

// 在设置 _recipeList 的值的函数中:
for (document in result) {
    val duration = document.data["duration"].toString()
    val name = document.data["name"].toString()
    var recipe = Recipe(name, duration)
    RecipesList.add(recipe)

    // 第二个数据库请求以获取配料
    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)
            }
        }

    // 第三个数据库请求以获取步骤
    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)
            }
        }
}
_recipeList.value = CustomResponse.Success(RecipeList)

在活动/片段中可以像这样做:

recipeList.observe(viewLifecycleOwner) { response ->
    when (response) {
        is CustomResponse.Loading -> {
            binding.recyclerViewRecipes.visibility = View.INVISIBLE
            binding.progressBar.visibility = View.VISIBLE
        }
        is CustomResponse.Success -> {
            binding.recyclerViewRecipes.visibility = View.VISIBLE
            binding.progressBar.visibility = View.GONE
            adapter.setList(response.data)
        }
    }
}

这里是 CustomResponse 密封类:

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)
}

如果食谱列表太大,可能会导致用户等待几分钟,那么你可以在 for 循环结束时设置 _reicpesList。这样会提升用户体验,用户无需等到所有食谱加载完毕。

英文:

You can use Live Data for that and manage state using Custom Response to Manage Success, Loading and error state when the data is loading show loader in UI you have to do something like this

  private val _recipeList = MutableLiveData&lt;CustomResponse&lt;List&lt;Recipe&gt;&gt;&gt;(CustomResponse.Loading())
val recipeList: LiveData&lt;CustomResponse&lt;List&lt;Recipe&gt;&gt;&gt;
    get() = _recipeList

In the function set value of _recipeList:

 for (document in result) {
                val duration = document.data[&quot;duration&quot;].toString()
                val name = document.data[&quot;name&quot;].toString()
                var recipe = Recipe(name, duration)
                RecipesList.add(recipe)

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

                //Third DB Request to get the Steps
             db.collection(&quot;users&quot;).document(userID.toString()).collection(&quot;Recipes&quot;).document(document.id).collection(&quot;steps&quot;).get()
                    .addOnSuccessListener { result -&gt;
                        for (document in result) {
                            val step = document.data[&quot;text&quot;].toString()
                            recipe.steps.add(step)
                        }
                    }
            }
 _recipeList.value = CustomResponse.Success(RecipeList)

and in activity/fragment do something like this :

recipeList.observe(viewLifecycleOwner) { response -&gt;
            when (response) {
                is CustomResponse.Loading -&gt; {
                    binding.recylerViewReicepes.visibility = View.INVISIBLE
                    binding.progressBar.visibility = View.VISIBLE
                }
                is CustomResponse.Success -&gt; {recipeList -&gt;
                    binding.recylerViewReicepes.visibility = View.VISIBLE
                    binding.progressBar.visibility = View.GONE
                    adapter.setList(recipeList)
                 
  
                    }
                }

Here is the CustomResoponse sealed class :

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

And if the recipes list is too big which can cause user to wait for several minutes then you can set _reicpesList at the end of for loop this will improve user experience so that user don't have to wait until all the recipes are loaded

huangapple
  • 本文由 发表于 2023年7月20日 20:16:02
  • 转载请务必保留本文链接:https://go.coder-hub.com/76729762.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定