API 数据需要点击 2 次按钮才能显示数据 - Kotlin

API data takes 2 button clicks in order to display data - Kotlin

我尝试搜索 Whosebug,但找不到我的问题的答案。单击搜索按钮时,我希望应用程序显示 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 中获取数据的实际代码(可能在 viewModel#getWinePairings 中),但我猜它在您的按钮点击侦听器中是这样的:

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

这里的问题是您正在执行需要一段时间才能完成的异步调用,但您正试图通过读取 apiResponse [=] 的当前 value 来立即显示结果21=]。你不应该那样做,你应该使用更 反应性 的设计 observe 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)
    }
}

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

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

虽然您没有共享您的 ViewModel 代码,但我猜测您的 ViewModel 的 getWinePairings() 函数从 API 异步检索数据,然后更新名为 apiResponse 的 LiveData return 值。由于 API 响应在 return 之前需要一些时间,因此当您从点击侦听器调用片段的 pairedWinesList() 函数时,您的 apiResponse LiveData 仍将是空的.

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

More information in this question about asynchronous calls.