【问题标题】:ViewModel is duplicating items for recyclerviewViewModel 正在为 recyclerview 复制项目
【发布时间】:2020-08-07 08:27:26
【问题描述】:

我在我的项目中使用 Firebase firestore 分页和 ViewModel 类,我为 recyclerview 设置了一个 onScroll 侦听器,并在滚动时获取数据,但是当我导航到另一个片段并返回主片段时,整个项目都被复制了,我该如何解决这个问题??

这是我的代码

NewsViewModel.kt

class NewsViewModel : ViewModel() {

private val repo = FirebaseRepo(this)

val mutableLiveData = MutableLiveData<List<News>>()

fun getNewsList(tm:Timestamp): LiveData<List<News>> {
    repo.getNewsData(tm)
    return mutableLiveData
}
}

Repository.kt

class FirebaseRepo(private val viewModel: NewsViewModel) {

private val db = FirebaseFirestore.getInstance().collection("news")

fun getNewsData(tm: Timestamp) {
    val newsList = ArrayList<News>()
    if(viewModel.mutableLiveData.value != null) {
        newsList.addAll(viewModel.mutableLiveData.value!!)
    }
    db
        .orderBy("timestamp", Query.Direction.DESCENDING)
        .whereLessThan("timestamp",tm)
        .limit(6)
        .get()
        .addOnSuccessListener {
            Log.i("CodeCamp", it.toString())
            for (doc in it) {
                val imgUrl = doc.getString("imageUrl")
                val heading = doc.getString("headline")
                val timestamp = doc.getTimestamp("timestamp")
                val tagline = doc.getString("tagline")
                val type = doc.getString("type")
                newsList.add(News(doc.id, imgUrl!!, heading!!, tagline!!, type!!, timestamp!!))
            }
            viewModel.mutableLiveData.value = newsList
        }
}
}

MainActivity.kt

viewModel = ViewModelProvider(this).get(NewsViewModel::class.java)
    val layoutManager = LinearLayoutManager(view.context)
    recyclerView.layoutManager = layoutManager
    recyclerView.adapter = newsAdapter
    recyclerView.addItemDecoration(DividerItemDecoration(view.context, RecyclerView.VERTICAL))

    //observe to the viewModel
    viewModel.getNewsList(Timestamp.now()).observe(viewLifecycleOwner, Observer {
        newsAdapter.submitList(it)
    })

    recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
        override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
            super.onScrolled(recyclerView, dx, dy)
            val visibleItemCount = layoutManager.childCount
            val totalItemCount = layoutManager.itemCount
            val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
            if ((visibleItemCount + firstVisibleItemPosition) >= totalItemCount
                && firstVisibleItemPosition >= 0
                && totalItemCount >= PAGE_SIZE && !isLoading
            ) {
                isLoading != isLoading
                val list = viewModel.mutableLiveData.value!!
                viewModel.getNewsList(list[list.size - 1].timestamp).value
                Handler().postDelayed({
                    isLoading != isLoading
                },2000)
            }
        }
    })

我的适配器

class NewsAdapter : ListAdapter<News, NewsAdapter.ViewHolder> (NEWS_COMPARATOR) {

companion object {
    private val NEWS_COMPARATOR =  object : DiffUtil.ItemCallback<News>() {
        override fun areItemsTheSame(old: News, new: News): Boolean = old.id == new.id
        override fun areContentsTheSame(old: News, new: News): Boolean = old == new
    }
}

class ViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {

    fun bindView(news: News) {
        Glide.with(view).load(news.imageUrl).into(itemView.img)
        itemView.news_title.text = news.heading
        itemView.news_src.text = news.tagline
        itemView.news_type.text = news.type
        itemView.news_time.text = DateTime.getTimeAgo(news.timestamp.seconds)
        itemView.setOnClickListener {
            it.findNavController().navigate(R.id.action_homeFragment_to_newsFragment, bundleOf("id" to news.id))
        }
    }
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    val view = LayoutInflater.from(parent.context).inflate(R.layout.item_news,parent,false)
    return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    val news = getItem(position)
    holder.bindView(news)
}

}

【问题讨论】:

    标签: android android-recyclerview viewmodel android-paging mutablelivedata


    【解决方案1】:

    LiveData 旨在保存数据,每次订阅它都会返回它当前拥有的数据。回到片段后,已保存在 LiveData 中的数据将再次传回。

    您可以通过几种不同的方式解决此问题: 您可以使用 SingleLiveEvents 来包装您的列表,并在每次在片段中收到新数据时检查数据是否被使用。如果未使用,则表示来自 ViewModel 的新数据。我使用这样的东西:

    class SingleLiveData<T>(dataToBeConsumed: T? = null) {
        private var _data: T? = dataToBeConsumed
    
        val isConsumed
            get() = _data == null
    
        fun consumeData(): T {
            val curData = _data!!
            _data = null
            return curData
        }
    
        fun consumeDataSafely(): T? {
            val curData = _data
            _data = null
            return curData
        }
    }
    

    这将导致修改 ViewModel 并改为:

    val mutableLiveData = MutableLiveData<SingleLiveData<List<News>>>()
    

    改变你填充数据的方式,比如

    viewModel.mutableLiveData.value = SingleLiveData(newsList)
    

    在您的代码中,您将在更新 RecyclerView 之前检查数据 isConsumed

    //observe to the viewModel
    viewModel.getNewsList(Timestamp.now()).observe(viewLifecycleOwner, Observer {
        // Now it is SingleLiveData<List>
        if (!it. isConsumed)
         newsAdapter.submitList(it.consumeData())
    })
    

    您可以浏览更多关于该主题的内容:https://proandroiddev.com/singleliveevent-to-help-you-work-with-livedata-and-events-5ac519989c70

    另一种方法是在更新您的 recyclerView 时使用 DiffUtil,这只会导致更新新对象而没有重复项。 参考:https://blog.mindorks.com/the-powerful-tool-diff-util-in-recyclerview-android-tutorial

    与您的问题无关,我建议不要在 FirebaseRepo 中保留 ViewModel 引用,而是使用回调 lambda 函数返回数据。您正在创建一个循环依赖项,这可能会导致您的应用出现错误和问题。

    【讨论】:

    • 你能写完整的代码吗,因为我是 SingleLiveData 的新手,你分享的文档是用 java 语言写的。..
    • SingleLiveData 实现足以让您入门。我已经编辑了我的答案,以展示如何观察数据的示例。如果您需要任何其他帮助,请告诉我。
    • onScroll Recylerviewonscroll listener val list = viewModel.mutableLiveData.value!!.consumeData() viewModel.getNewsList(list[list.size - 1].timestamp) 我收到错误 kotlin.KotlinNullPointerException
    • 还有在哪里使用consumeDataSafely()和isConsumed?
    • 我刚刚注意到您正在从 onScroll 内部调用 getNewList。您应该更改此方法并访问已在 RecyclerView 中的数据。您可以使用以下方法获取最后一个时间戳: val lastItem = newsAdapter.items[newsAdapter.items.size - 1] (其中 items 是适配器内的 ArrayList)您现在可以忽略 consumeDataSafely(),如果您不确定您的数据是否为空。 isConsumed 在调用 newsAdapter 之前使用,如示例中所示 if (!it.isConsumed) newsAdapter.submitList(it.consumeData()) @nirazverma
    【解决方案2】:

    问题在于你的解构:

    每当 Observer 调用您的 ViewModel 时,都会从 firebase 重新获取数据,并将其保存在您定义的名为 mutableLivedata 的变量中。

    您需要观察 recyclerView 的 mutableLiveData 并在 init 函数中调用 getNewsItem(),如下所示:

    ViewModel.kt

    val mutableLiveData = MutableLiveData<List<News>>()
    
    fun getNewsList(tm:Timestamp) {
        repo.getNewsData(tm)
    }
    
    init {
        getNewsList(Timestamp.now())
    }
    

    MainActivity.kt

    viewModel.mutableLiveData.observe(viewLifecycleOwner, Observer {
            newsAdapter.submitList(it)
        })
    

    快乐编码..

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-02-20
      • 2021-12-20
      • 1970-01-01
      • 2017-04-22
      • 2018-06-30
      • 2021-04-09
      • 2017-06-21
      相关资源
      最近更新 更多