问题在 Recyclerview 上使用 LiveData、Retrofit、Coroutine 实施更新 UI:适配器 Recyclerview 未更新
Problem Implement Update UI with LiveData, Retrofit, Coroutine on Recyclerview : adapter Recyclerview not update
我是新手,在我的开发应用程序中使用 Kotlin android,
现在,我正在学习使用 LiveData、Retrofit、Coroutine 在 Recyclerview 上实现 Update UI。我的应用程序:
MainActivity > MainFragment with 3 Tab fragment > HomeFragment, DashboardFragment, and SettingsFragment
我在 onCreateView HomeFragment 上调用函数从服务器获取数据,并在加载时在我的 Recylerview 上显示微光数据观察这一点,成功时尝试更新 RecyclerView,并显示视图出错时加载按钮刷新失败。
问题是:
- 成功从服务器获取数据后,适配器 Recyclerview 未更新。适配器仍然显示微光数据
- 如果出现大小写错误(无互联网),我会显示加载失败视图,并带有刷新按钮。点击刷新,我重新调用函数来获取数据服务器,但函数无法正常工作。 Recyclerview 显示最后的数据,不再显示失败的加载。
下面是我的代码
HomeFragment
private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!!
private lateinit var adapterNews: NewsAdapter
private var shimmerNews: Boolean = false
private var itemsDataNews = ArrayList<NewsModel>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout for this fragment
_binding = FragmentHomeBinding.inflate(inflater, container, false)
......
newsViewModel = ViewModelProvider(requireActivity()).get(NewsViewModel::class.java)
//news
binding.frameNews.rvNews.setHasFixedSize(true)
binding.frameNews.rvNews.layoutManager = llmh
adapterNews = NewsAdapter(itemsDataNews, shimmerNews)
binding.frameNews.rvNews.adapter = adapterNews
// Observe
//get News
newsViewModel.refresh()
newsViewModel.newsList.observe(
viewLifecycleOwner,
androidx.lifecycle.Observer { newsList ->
newsList?.let {
binding.frameNews.rvNews.visibility = View.VISIBLE
binding.frameNews.rvNews.isNestedScrollingEnabled = true
binding.frameNews.itemNewsLayoutFailed.visibility = View.GONE
if (it.size == 0)
binding.frameNews.root.visibility = View.GONE
else
getDataNews(it)
}
})
newsViewModel.loading.observe(viewLifecycleOwner) { isLoading ->
isLoading?.let {
binding.frameNews.rvNews.visibility = View.VISIBLE
binding.frameNews.rvNews.isNestedScrollingEnabled = false
binding.frameNews.itemNewsLayoutFailed.visibility = View.GONE
getDataNewsShimmer()
}
}
newsViewModel.loadError.observe(viewLifecycleOwner) { isError ->
isError?.let {
binding.frameNews.rvNews.visibility = View.INVISIBLE
binding.frameNews.itemNewsLayoutFailed.visibility = View.VISIBLE
binding.frameNews.btnNewsFailed.setOnClickListener {
newsViewModel.refresh()
}
}
}
....
return binding.root
}
@SuppressLint("NotifyDataSetChanged")
private fun getDataNewsShimmer() {
shimmerNews = true
itemsDataNews.clear()
itemsDataNews.addAll(NewsData.itemsShimmer)
adapterNews.notifyDataSetChanged()
}
@SuppressLint("NotifyDataSetChanged")
private fun getDataNews(list: List<NewsModel>) {
Toast.makeText(requireContext(), list.size.toString(), Toast.LENGTH_SHORT).show()
shimmerNews = false
itemsDataNews.clear()
itemsDataNews.addAll(list)
adapterNews.notifyDataSetChanged()
}
override fun onDestroyView() {
super.onDestroyView()
_binding=null
}
NewsViewModel
class NewsViewModel: ViewModel() {
val newsService = KopraMobileService().getNewsApi()
var job: Job? = null
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
onError("Exception handled: ${throwable.localizedMessage}")
}
val newsList = MutableLiveData<List<NewsModel>>()
val loadError = MutableLiveData<String?>()
val loading = MutableLiveData<Boolean>()
fun refresh() {
fetchNews()
}
private fun fetchNews() {
loading.postValue(true)
job = CoroutineScope(Dispatchers.IO + exceptionHandler).launch {
val response = newsService.getNewsList()
withContext(Dispatchers.Main) {
if (response.isSuccessful) {
newsList.postValue(response.body()?.data)
loadError.postValue(null)
loading.postValue(false)
} else {
onError("Error : ${response.message()} ")
}
}
}
loadError.postValue("")
loading.postValue( false)
}
private fun onError(message: String) {
loadError.postValue(message)
loading.postValue( false)
}
override fun onCleared() {
super.onCleared()
job?.cancel()
}
}
新闻适配器
NewsAdapter(
var itemsCells: List<NewsModel?> = emptyList(),
var shimmer: Boolean ,
) :
RecyclerView.Adapter<ViewHolder>() {
private lateinit var context: Context
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NewsHomeViewHolder {
context = parent.context
return NewsHomeViewHolder(
NewsItemHomeBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
}
override fun onBindViewHolder(holder: NewsHomeViewHolder, position: Int) {
holder.bind(itemsCells[position]!!)
}
inner class NewsHomeViewHolder(private val binding: NewsItemHomeBinding) :
ViewHolder(binding.root) {
fun bind(newsItem: NewsModel) {
binding.newsItemFlat.newsTitle.text = newsItem.name
binding.newsItemFlatShimmer.newsTitle.text = newsItem.name
binding.newsItemFlat.newsSummary.text = newsItem.name
binding.newsItemFlatShimmer.newsSummary.text = newsItem.name
if (shimmer) {
binding.layoutNewsItemFlat.visibility = View.INVISIBLE
binding.layoutNewsItemFlatShimmer.visibility = View.VISIBLE
binding.layoutNewsItemFlatShimmer.startShimmer()
} else {
binding.layoutNewsItemFlat.visibility = View.VISIBLE
binding.layoutNewsItemFlatShimmer.visibility = View.INVISIBLE
}
}
}
override fun getItemCount(): Int {
return itemsCells.size
}
希望有人能帮我解决问题。谢谢,对不起我的英语。
你有 3 个不同的 LiveData
,对吧?一个是新闻数据列表,一个是加载错误信息,一个是加载状态。
您为其中的每一个都设置了一个 Observer
,并且只要 LiveData
的值更新 ,就会调用该观察函数 。这很重要,因为当您的请求成功并获得一些新数据时会发生以下情况:
if (response.isSuccessful) {
newsList.postValue(response.body()?.data)
loadError.postValue(null)
loading.postValue(false)
}
表示newsList
更新,然后 loadError
更新,然后 loading
更新。
因此,您的观察者按顺序为 LiveData
中的每个 运行 函数。但是你没有测试新值(空检查除外),所以每个中的代码总是运行s值更新。因此,当您的响应成功时,会发生这种情况:
newsList
观察者运行s,显示成功,调用getDataNews
loadError
观察者 运行s,值为 null 所以没有任何反应
loading
观察者运行s,值为false但未检查,显示为正在加载 , 来电 getDataNewsShimmer
所以即使响应成功,你做的最后一件事就是显示加载状态
您需要先检查状态(如 loading
),然后再尝试显示它。但是如果你检查 loading
是 true,你将在 fetchNews
中有一个错误 - 设置 loading = true
,启动一个协程完成 之后,然后立即设置loading = false
.
我建议尝试创建一个 class 代表不同的 状态 ,用一个 LiveData
代表当前状态。像这样:
// a class representing the different states, and any data they need
sealed class State {
object Loading : State()
data class Success(val newsList: List<NewsModel>?) : State()
data class Error(val message: String) : State()
}
// this is just a way to keep the mutable LiveData private, so it can't be updated
private val _state = MutableLiveData<State>()
val state: LiveData<State> get() = _state
private fun fetchNews() {
// initial state is Loading, until we get a response
_state.value = State.Loading
job = CoroutineScope(Dispatchers.IO + exceptionHandler).launch {
val response = newsService.getNewsList()
// if you're using postValue I don't think you need to switch to Dispatchers.Main?
_state.postValue(
// when you get a response, the state is now either Success or Error
if (response.isSuccessful) State.Success(response.body()?.data)
else State.Error("Error : ${response.message()} ")
)
}
}
然后你只需要观察 state
:
// you don't need to create an Observer object, you can use a lambda!
newsViewModel.state.observe(viewLifecycleOwner) { state ->
// Handle the different possible states, and display the current one
// this lets us avoid repeating 'binding.frameNews' before everything
with(binding.frameNews) {
// You could use a when block, and describe each state explicitly,
// like your current setup:
when(state) {
// just checking equality because Loading is a -singleton object instance-
State.Loading -> {
rvNews.visibility = View.VISIBLE
rvNews.isNestedScrollingEnabled = false
itemNewsLayoutFailed.visibility = View.GONE
getDataNewsShimmer()
}
// Error and Success are both -classes- so we need to check their type with 'is'
is State.Error -> {
rvNews.visibility = View.INVISIBLE
itemNewsLayoutFailed.visibility = View.VISIBLE
btnNewsFailed.setOnClickListener {
newsViewModel.refresh()
}
}
is State.Success -> {
rvNews.visibility = View.VISIBLE
rvNews.isNestedScrollingEnabled = true
itemNewsLayoutFailed.visibility = View.GONE
// Because we know state is a Success, we can access newsList on it
// newsList can be null - I don't know how you want to handle that,
// I'm just treating it as defaulting to size == 0
// (make sure you make this visible when necessary too)
if (state.newsList?.size ?: 0 == 0) root.visibility = View.GONE
else getDataNews(state.newsList)
}
}
// or, if you like, you could do this kind of thing instead:
itemNewsLayoutFailed.visibility = if (state is Error) VISIBLE else GONE
}
}
您可能还想将该显示代码分解为单独的函数(如 showError()
、showList(state.newsList)
等)并从 when
的分支中调用它们,如果这样的话它更具可读性
我希望这是有道理的!当您有一个表示状态的单个值时,使用它会容易得多 - 在事情发生变化时设置当前状态,并让您的观察者通过更新显示来处理每个可能的 UI 状态。正在加载时,使其看起来像 this。当出现错误时,让它看起来像 this
这应该有助于避免您为多个事物多次更新,试图协调所有事物的错误。我不确定为什么您在错误后重新加载时会看到该问题,但这样做可能有助于修复它(或更容易查看导致它的原因)
我是新手,在我的开发应用程序中使用 Kotlin android, 现在,我正在学习使用 LiveData、Retrofit、Coroutine 在 Recyclerview 上实现 Update UI。我的应用程序:
MainActivity > MainFragment with 3 Tab fragment > HomeFragment, DashboardFragment, and SettingsFragment
我在 onCreateView HomeFragment 上调用函数从服务器获取数据,并在加载时在我的 Recylerview 上显示微光数据观察这一点,成功时尝试更新 RecyclerView,并显示视图出错时加载按钮刷新失败。
问题是:
- 成功从服务器获取数据后,适配器 Recyclerview 未更新。适配器仍然显示微光数据
- 如果出现大小写错误(无互联网),我会显示加载失败视图,并带有刷新按钮。点击刷新,我重新调用函数来获取数据服务器,但函数无法正常工作。 Recyclerview 显示最后的数据,不再显示失败的加载。
下面是我的代码
HomeFragment
private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!!
private lateinit var adapterNews: NewsAdapter
private var shimmerNews: Boolean = false
private var itemsDataNews = ArrayList<NewsModel>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout for this fragment
_binding = FragmentHomeBinding.inflate(inflater, container, false)
......
newsViewModel = ViewModelProvider(requireActivity()).get(NewsViewModel::class.java)
//news
binding.frameNews.rvNews.setHasFixedSize(true)
binding.frameNews.rvNews.layoutManager = llmh
adapterNews = NewsAdapter(itemsDataNews, shimmerNews)
binding.frameNews.rvNews.adapter = adapterNews
// Observe
//get News
newsViewModel.refresh()
newsViewModel.newsList.observe(
viewLifecycleOwner,
androidx.lifecycle.Observer { newsList ->
newsList?.let {
binding.frameNews.rvNews.visibility = View.VISIBLE
binding.frameNews.rvNews.isNestedScrollingEnabled = true
binding.frameNews.itemNewsLayoutFailed.visibility = View.GONE
if (it.size == 0)
binding.frameNews.root.visibility = View.GONE
else
getDataNews(it)
}
})
newsViewModel.loading.observe(viewLifecycleOwner) { isLoading ->
isLoading?.let {
binding.frameNews.rvNews.visibility = View.VISIBLE
binding.frameNews.rvNews.isNestedScrollingEnabled = false
binding.frameNews.itemNewsLayoutFailed.visibility = View.GONE
getDataNewsShimmer()
}
}
newsViewModel.loadError.observe(viewLifecycleOwner) { isError ->
isError?.let {
binding.frameNews.rvNews.visibility = View.INVISIBLE
binding.frameNews.itemNewsLayoutFailed.visibility = View.VISIBLE
binding.frameNews.btnNewsFailed.setOnClickListener {
newsViewModel.refresh()
}
}
}
....
return binding.root
}
@SuppressLint("NotifyDataSetChanged")
private fun getDataNewsShimmer() {
shimmerNews = true
itemsDataNews.clear()
itemsDataNews.addAll(NewsData.itemsShimmer)
adapterNews.notifyDataSetChanged()
}
@SuppressLint("NotifyDataSetChanged")
private fun getDataNews(list: List<NewsModel>) {
Toast.makeText(requireContext(), list.size.toString(), Toast.LENGTH_SHORT).show()
shimmerNews = false
itemsDataNews.clear()
itemsDataNews.addAll(list)
adapterNews.notifyDataSetChanged()
}
override fun onDestroyView() {
super.onDestroyView()
_binding=null
}
NewsViewModel
class NewsViewModel: ViewModel() {
val newsService = KopraMobileService().getNewsApi()
var job: Job? = null
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
onError("Exception handled: ${throwable.localizedMessage}")
}
val newsList = MutableLiveData<List<NewsModel>>()
val loadError = MutableLiveData<String?>()
val loading = MutableLiveData<Boolean>()
fun refresh() {
fetchNews()
}
private fun fetchNews() {
loading.postValue(true)
job = CoroutineScope(Dispatchers.IO + exceptionHandler).launch {
val response = newsService.getNewsList()
withContext(Dispatchers.Main) {
if (response.isSuccessful) {
newsList.postValue(response.body()?.data)
loadError.postValue(null)
loading.postValue(false)
} else {
onError("Error : ${response.message()} ")
}
}
}
loadError.postValue("")
loading.postValue( false)
}
private fun onError(message: String) {
loadError.postValue(message)
loading.postValue( false)
}
override fun onCleared() {
super.onCleared()
job?.cancel()
}
}
新闻适配器
NewsAdapter(
var itemsCells: List<NewsModel?> = emptyList(),
var shimmer: Boolean ,
) :
RecyclerView.Adapter<ViewHolder>() {
private lateinit var context: Context
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NewsHomeViewHolder {
context = parent.context
return NewsHomeViewHolder(
NewsItemHomeBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
}
override fun onBindViewHolder(holder: NewsHomeViewHolder, position: Int) {
holder.bind(itemsCells[position]!!)
}
inner class NewsHomeViewHolder(private val binding: NewsItemHomeBinding) :
ViewHolder(binding.root) {
fun bind(newsItem: NewsModel) {
binding.newsItemFlat.newsTitle.text = newsItem.name
binding.newsItemFlatShimmer.newsTitle.text = newsItem.name
binding.newsItemFlat.newsSummary.text = newsItem.name
binding.newsItemFlatShimmer.newsSummary.text = newsItem.name
if (shimmer) {
binding.layoutNewsItemFlat.visibility = View.INVISIBLE
binding.layoutNewsItemFlatShimmer.visibility = View.VISIBLE
binding.layoutNewsItemFlatShimmer.startShimmer()
} else {
binding.layoutNewsItemFlat.visibility = View.VISIBLE
binding.layoutNewsItemFlatShimmer.visibility = View.INVISIBLE
}
}
}
override fun getItemCount(): Int {
return itemsCells.size
}
希望有人能帮我解决问题。谢谢,对不起我的英语。
你有 3 个不同的 LiveData
,对吧?一个是新闻数据列表,一个是加载错误信息,一个是加载状态。
您为其中的每一个都设置了一个 Observer
,并且只要 LiveData
的值更新 ,就会调用该观察函数 。这很重要,因为当您的请求成功并获得一些新数据时会发生以下情况:
if (response.isSuccessful) {
newsList.postValue(response.body()?.data)
loadError.postValue(null)
loading.postValue(false)
}
表示newsList
更新,然后 loadError
更新,然后 loading
更新。
因此,您的观察者按顺序为 LiveData
中的每个 运行 函数。但是你没有测试新值(空检查除外),所以每个中的代码总是运行s值更新。因此,当您的响应成功时,会发生这种情况:
newsList
观察者运行s,显示成功,调用getDataNews
loadError
观察者 运行s,值为 null 所以没有任何反应loading
观察者运行s,值为false但未检查,显示为正在加载 , 来电getDataNewsShimmer
所以即使响应成功,你做的最后一件事就是显示加载状态
您需要先检查状态(如 loading
),然后再尝试显示它。但是如果你检查 loading
是 true,你将在 fetchNews
中有一个错误 - 设置 loading = true
,启动一个协程完成 之后,然后立即设置loading = false
.
我建议尝试创建一个 class 代表不同的 状态 ,用一个 LiveData
代表当前状态。像这样:
// a class representing the different states, and any data they need
sealed class State {
object Loading : State()
data class Success(val newsList: List<NewsModel>?) : State()
data class Error(val message: String) : State()
}
// this is just a way to keep the mutable LiveData private, so it can't be updated
private val _state = MutableLiveData<State>()
val state: LiveData<State> get() = _state
private fun fetchNews() {
// initial state is Loading, until we get a response
_state.value = State.Loading
job = CoroutineScope(Dispatchers.IO + exceptionHandler).launch {
val response = newsService.getNewsList()
// if you're using postValue I don't think you need to switch to Dispatchers.Main?
_state.postValue(
// when you get a response, the state is now either Success or Error
if (response.isSuccessful) State.Success(response.body()?.data)
else State.Error("Error : ${response.message()} ")
)
}
}
然后你只需要观察 state
:
// you don't need to create an Observer object, you can use a lambda!
newsViewModel.state.observe(viewLifecycleOwner) { state ->
// Handle the different possible states, and display the current one
// this lets us avoid repeating 'binding.frameNews' before everything
with(binding.frameNews) {
// You could use a when block, and describe each state explicitly,
// like your current setup:
when(state) {
// just checking equality because Loading is a -singleton object instance-
State.Loading -> {
rvNews.visibility = View.VISIBLE
rvNews.isNestedScrollingEnabled = false
itemNewsLayoutFailed.visibility = View.GONE
getDataNewsShimmer()
}
// Error and Success are both -classes- so we need to check their type with 'is'
is State.Error -> {
rvNews.visibility = View.INVISIBLE
itemNewsLayoutFailed.visibility = View.VISIBLE
btnNewsFailed.setOnClickListener {
newsViewModel.refresh()
}
}
is State.Success -> {
rvNews.visibility = View.VISIBLE
rvNews.isNestedScrollingEnabled = true
itemNewsLayoutFailed.visibility = View.GONE
// Because we know state is a Success, we can access newsList on it
// newsList can be null - I don't know how you want to handle that,
// I'm just treating it as defaulting to size == 0
// (make sure you make this visible when necessary too)
if (state.newsList?.size ?: 0 == 0) root.visibility = View.GONE
else getDataNews(state.newsList)
}
}
// or, if you like, you could do this kind of thing instead:
itemNewsLayoutFailed.visibility = if (state is Error) VISIBLE else GONE
}
}
您可能还想将该显示代码分解为单独的函数(如 showError()
、showList(state.newsList)
等)并从 when
的分支中调用它们,如果这样的话它更具可读性
我希望这是有道理的!当您有一个表示状态的单个值时,使用它会容易得多 - 在事情发生变化时设置当前状态,并让您的观察者通过更新显示来处理每个可能的 UI 状态。正在加载时,使其看起来像 this。当出现错误时,让它看起来像 this
这应该有助于避免您为多个事物多次更新,试图协调所有事物的错误。我不确定为什么您在错误后重新加载时会看到该问题,但这样做可能有助于修复它(或更容易查看导致它的原因)