问题在 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,并显示视图出错时加载按钮刷新失败。

问题是:

下面是我的代码

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),然后再尝试显示它。但是如果你检查 loadingtrue,你将在 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

这应该有助于避免您为多个事物多次更新,试图协调所有事物的错误。我不确定为什么您在错误后重新加载时会看到该问题,但这样做可能有助于修复它(或更容易查看导致它的原因)