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.
我尝试搜索 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.