【问题标题】:API data takes 2 button clicks in order to display data - KotlinAPI 数据需要单击 2 次按钮才能显示数据 - Kotlin
【发布时间】:2022-01-26 01:42:27
【问题描述】:

我已尝试搜索 stackoverflow,但找不到我的问题的答案。单击搜索按钮时,我希望应用程序显示来自 API 的数据。我遇到的问题是它需要点击 2 次搜索按钮才能显示数据。第一次单击显示“null”,第二次单击正确显示所有数据。我究竟做错了什么?为了在第一次点击时正确处理,我需要更改什么?提前致谢!

配对片段

package com.example.winepairing.view.fragments

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.activityViewModels
import com.example.winepairing.databinding.FragmentPairingBinding
import com.example.winepairing.utils.hideKeyboard
import com.example.winepairing.viewmodel.PairingsViewModel



class PairingFragment : Fragment() {
    private var _binding: FragmentPairingBinding? = null
    private val binding get() = _binding!!
    private val viewModel: PairingsViewModel by activityViewModels()


    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        _binding = FragmentPairingBinding.inflate(inflater, container, false)
        val view = binding.root

        val toolbar = binding.toolbar
        (activity as AppCompatActivity).setSupportActionBar(toolbar)

        binding.searchBtn.setOnClickListener {
            hideKeyboard()
            if (binding.userItem.text.isNullOrEmpty()) {
                Toast.makeText(this@PairingFragment.requireActivity(),
                    "Please enter a food, entree, or cuisine",
                    Toast.LENGTH_SHORT).show()
            } else {
                val foodItem = binding.userItem.text.toString()
                getWinePairing(foodItem)
                pairedWinesList()
                pairingInfo()
            }
        }
        return view
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }

    private fun pairedWinesList() {
        val pairedWines = viewModel.apiResponse.value?.pairedWines
        var content = ""
        if (pairedWines != null) {
            for (i in 0 until pairedWines.size) {
                //Append all the values to a string
                content += pairedWines.get(i)
                content += "\n"
            }
        }
        binding.pairingWines.setText(content)
    }

    private fun pairingInfo() {
        val pairingInfo = viewModel.apiResponse.value?.pairingText.toString()
        binding.pairingInfo.setText(pairingInfo)
    }

    private fun getWinePairing(foodItem: String) {
        viewModel.getWinePairings(foodItem.lowercase())

    }
}


所以,对不起!!!这是视图模型

package com.example.winepairing.viewmodel

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.winepairing.BuildConfig
import com.example.winepairing.model.data.Wine
import com.example.winepairing.model.network.WineApi
import kotlinx.coroutines.launch


const val CLIENT_ID = BuildConfig.SPOONACULAR_ACCESS_KEY
class PairingsViewModel: ViewModel() {
    private val _apiResponse = MutableLiveData<Wine>()
    val apiResponse: LiveData<Wine> = _apiResponse

    fun getWinePairings(food: String) {
        viewModelScope.launch {
            _apiResponse.value = WineApi.retrofitService.getWinePairing(food, CLIENT_ID)
        }
    }
}

【问题讨论】:

    标签: api kotlin button click


    【解决方案1】:

    您尚未发布用于从 API 获取数据的实际代码(可能在 viewModel#getWinePairings 中),但猜测在您的按钮单击侦听器中是这样的:

    • 您调用getWinePairing - 这将启动一个异步调用,该调用最终将在viewModel.apiResponse 上完成并设置数据,在未来的某个时间。它的初始值为null
    • 您调用pairedWinesList,它引用了apiResponse 的当前值——在API 调用为其设置值之前,该值将为空。由于您基本上是在获取最新的完成搜索结果,因此如果您更改搜索数据,那么您最终会显示 previous 搜索的结果,而 API调用在后台运行并稍后更新apiResponse
    • 您调用 pairingInfo() 与上述相同,您正在查看 API 调用返回新结果之前的陈旧值

    这里的问题是您正在执行需要一段时间才能完成的异步调用,但您试图通过读取当前的 valueapiResponse LiveData 来立即显示结果。你不应该这样做,你应该使用更响应的设计,observes LiveData,并在发生某些事情时更新你的 UI(即当你得到新结果时):

    // in onCreateView, set everything up
    
    viewModel.apiResponse.observe(viewLifeCycleOwner) { response ->
        // this is called when a new 'response' comes in, so you can
        // react to that by updating your UI as appropriate
        binding.pairingInfo.setText(response.pairingInfo.toString())
        // this is a way you can combine all your wine strings FYI
        val wines = response.pairedWines?.joinToString(separator="\n") ?: ""
        binding.pairingWines.setText(wines)
    }
            
    binding.searchBtn.setOnClickListener {
        ...
        } else {
            val foodItem = binding.userItem.text.toString()
            // kick off the API request - we don't display anything here, the observing
            // function above handles that when we get the results back
            getWinePairing(foodItem)
        }
    }
    

    希望这是有道理的 - 按钮单击侦听器只是启动异步获取操作(可能是网络调用,或慢速数据库调用,或不会阻塞线程的快速内存获取 - 片段不会'不需要知道细节!)并且observe函数处理在UI中显示新状态,无论何时到达。

    优点是您将所有内容都分离出来 - 视图模型处理状态,UI 只处理点击(更新视图模型)和显示新状态(对视图模型中的更改做出反应)之类的事情。这样你也可以做一些事情,比如让 VM 保存自己的状态,当它初始化时,UI 只会对这个变化做出反应并自动显示它。它不需要知道是什么导致了这种变化,你知道吗?

    【讨论】:

    • 谢谢!你真的帮助我理解了我的错误!你的解决方案奏效了!非常非常感谢!
    【解决方案2】:

    虽然您没有分享您的 ViewModel 代码,但我猜测您的 ViewModel 的 getWinePairings() 函数从 API 异步检索数据,然后使用返回值更新名为 apiResponse 的 LiveData。由于 API 响应需要一些时间才能返回,所以当您从点击侦听器调用 Fragment 的 pairedWinesList() 函数时,您的 apiResponse LiveData 仍将为空。

    提示,每当您在管理它的 ViewModel 之外使用 LiveData 的 .value 时,您可能做错了什么。 LiveData 的重点是在数据到达时对其做出反应,因此您应该对其调用observe(),而不是尝试同步读取其.value

    More information in this question about asynchronous calls.

    【讨论】:

    • 谢谢!我现在了解在视图模型之外使用 .value 的问题。
    猜你喜欢
    • 2018-06-29
    • 2019-04-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-02-01
    • 2017-09-23
    • 1970-01-01
    • 2014-08-08
    相关资源
    最近更新 更多