You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ListAdapter提交列表未更新,删除数据出现重复项求助

Android ListAdapter 删除数据后出现重复行的问题修复

问题描述

使用ListAdapter实现RecyclerView列表时,删除任意行数据后会出现列表重复渲染的情况:比如删除首行后,首行仍保留且第二行重复;删除其他行也会出现数据重复展示的问题。

问题根源分析

从提供的代码来看,主要问题集中在数据一致性维护和LiveData监听逻辑上:

  • Fragment中convertedArticleList在LiveData回调里未清空旧数据就追加新数据,导致多次触发时数据重复累积
  • 每次onResume重复注册LiveData Observer,加重数据重复问题
  • 手动调用Adapter的deleteItem修改内部列表,但Fragment数据源未同步,后续LiveData触发时旧数据被再次添加
  • Article类可能未正确实现equals方法,导致DiffUtil无法准确识别内容变化

解决方案

1. 修复Fragment的数据监听与更新逻辑

将LiveData Observer移至onViewCreated仅注册一次,每次收到新数据时先清空列表再添加,确保数据为最新完整集合:

class SavedNews : Fragment() {

    lateinit var mymainViewModel: MainViewModel
    lateinit var databaseObject: MyDataBase
    lateinit var myAdapter: MyListAdapter
    lateinit var convertedArticleList : ArrayList<Article>

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        myAdapter = MyListAdapter()
        convertedArticleList = ArrayList()
        return inflater.inflate(R.layout.fragment_saved_news, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        savedNewsRecyclerView.layoutManager = LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
        savedNewsRecyclerView.adapter = myAdapter

        // 初始化ViewModel与Repository,避免重复创建
        val myInterfaceObject = ApiInterface.MyObject.getInstance()
        databaseObject = MyDataBase.MyObjectDB.getDBInstance(activity as MainActivity)
        val myRepository = Repository(myInterfaceObject, databaseObject)
        mymainViewModel = ViewModelProvider(
            this,
            MainViewModelFactory(myRepository)
        ).get(MainViewModel::class.java)

        // 仅注册一次LiveData Observer
        mymainViewModel.abstractSavedNews().observe(viewLifecycleOwner, Observer { likedArticles ->
            convertedArticleList.clear() // 清空旧数据,避免重复
            likedArticles.forEach { eachLikedArticle ->
                val obj = toArticle(eachLikedArticle)
                convertedArticleList.add(obj)
            }
            myAdapter.submitList(convertedArticleList.toList()) // 提交列表副本
        })

        // 滑动删除逻辑
        ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.RIGHT) {
            override fun onMove(
                recyclerView: RecyclerView,
                viewHolder: RecyclerView.ViewHolder,
                target: RecyclerView.ViewHolder
            ): Boolean {
                return false
            }

            override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
                val position = viewHolder.adapterPosition
                val deletedArticle = convertedArticleList[position]
                // 直接操作数据库,依赖LiveData自动更新列表
                mymainViewModel.deleteSavedNews(deletedArticle.id!!)
                
                // 撤销功能:通过插入数据库触发LiveData更新
                Snackbar.make(savedNewsRecyclerView, "已删除 ${deletedArticle.title}", Snackbar.LENGTH_LONG)
                    .setAction("撤销") {
                        mymainViewModel.saveNews(toLikedArticle(deletedArticle))
                    }.show()
            }
        }).attachToRecyclerView(savedNewsRecyclerView)

        // 列表项点击事件
        myAdapter.setOnItemClickListener { article ->
            val bundle = Bundle().apply {
                putSerializable("article", article)
            }
            findNavController().navigate(R.id.action_savedNews_to_article, bundle)
        }
    }

    // Article转LikedArticle,用于撤销操作
    private fun toLikedArticle(article: Article) = LikedArticle(
        article.id, article.author, article.content,
        article.description, article.publishedAt, article.source, article.title,
        article.url, article.urlToImage
    )

    private fun toArticle(rawObject: LikedArticle) = Article(
        rawObject.author, rawObject.content,
        rawObject.description, rawObject.publishedAt, rawObject.source, rawObject.title,
        rawObject.url, rawObject.urlToImage, rawObject.id
    )
}

2. 简化Adapter实现,移除内部数据修改逻辑

ListAdapter设计初衷是通过外部提交新列表驱动更新,删除Adapter内的deleteItem方法:

class MyListAdapter : ListAdapter<Article, MyListAdapter.MyViewHolder>(MyDiffUtil()) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.recycler_item, parent, false)
        return MyViewHolder(view)
    }

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        val item = getItem(position)
        holder.onBind(item)
        holder.itemView.setOnClickListener {
            onItemClickListener?.invoke(item)
        }
    }

    class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        fun onBind(article: Article) = with(itemView) {
            Glide.with(this)
                .load(article.urlToImage)
                .error(R.drawable.notfound)
                .into(imageView)

            webSource.text = article.source?.name
            newsDate.text = article.publishedAt
            newsTitle.text = article.title
            newsDescription.text = article.description
        }
    }

    private var onItemClickListener: ((Article) -> Unit)? = null
    fun setOnItemClickListener(listener: (Article) -> Unit) {
        onItemClickListener = listener
    }

    class MyDiffUtil : DiffUtil.ItemCallback<Article>() {
        override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean {
            // 用唯一ID判断是否为同一条目
            return oldItem.id == newItem.id
        }

        override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean {
            // 依赖Article类正确实现equals方法
            return oldItem == newItem
        }
    }
}

3. 确保Article类正确实现equals和hashCode方法

DiffUtil的areContentsTheSame依赖equals判断内容一致性,若使用普通类需手动重写,Kotlin数据类会自动生成:

// Kotlin数据类自动生成equals和hashCode
data class Article(
    val author: String?,
    val content: String?,
    val description: String?,
    val publishedAt: String?,
    val source: Source?,
    val title: String?,
    val url: String?,
    val urlToImage: String?,
    val id: Int?
)

data class Source(
    val name: String?
)

关键说明

  • 依赖LiveData自动更新:通过操作数据库触发Room的LiveData通知,Fragment收到新数据后更新Adapter,保证数据源一致性
  • 提交列表副本:调用submitList时传入列表副本,确保每次都是新实例,DiffUtil能正确对比差异并触发动画
  • DiffUtil正确配置:areItemsTheSame用唯一ID判断条目身份,areContentsTheSame依赖正确的equals方法判断内容变化,实现流畅的删除动画

内容的提问来源于stack exchange,提问作者Sijan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.07 18:40:56