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

更新Firebase后如何保留RecyclerView的滚动位置

问题解决:更新Firebase后RecyclerView重置到顶部

问题根源

每次修改Firebase中的角色数据后,addSnapshotListener会触发回调,重新生成完整的角色列表并更新LiveData。而你的Activity在观察到LiveData变化时,每次都创建新的CharacterAdapter并设置给RecyclerView,这会导致RecyclerView重新初始化,自然滚回顶部。

另外还有两处逻辑问题会加重这个情况:

  • Adapter中直接修改character.spoken字段,没有通过数据驱动UI更新
  • Activity中调用notifyItemChanged的时机不对,且新Adapter创建后之前的通知无效

解决方案

1. 只初始化一次Adapter,复用实例

在Activity中,不要每次数据变化都创建新Adapter,而是初始化一次,之后仅更新Adapter的数据集合。

修改Activity代码:

class DailiesActivity : AppCompatActivity(), CharacterAdapter.CharacterItemListener {

    private lateinit var binding : ActivityDailiesBinding
    private lateinit var auth : FirebaseAuth
    private val db = FirebaseFirestore.getInstance()
    // 声明Adapter变量,复用实例
    private lateinit var characterAdapter: CharacterAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityDailiesBinding.inflate(layoutInflater)
        setContentView(binding.root)

        auth = Firebase.auth
        val recyclerView = binding.charactersRecyclerView
        val viewModel : CharacterViewModel by viewModels()

        // 初始化一次Adapter
        characterAdapter = CharacterAdapter(this, emptyList(), this, recyclerView)
        recyclerView.adapter = characterAdapter

        // 观察数据变化时,仅更新Adapter的数据集
        viewModel.getCharacters().observe(this) { characters ->
            characterAdapter.updateData(characters)
        }

        setSupportActionBar(binding.mainToolBar.toolbar)
        supportActionBar?.title = ""
    }

    // ... 其他代码不变

    override fun characterSelected(character: Character, adapterPosition: Int) {
        Log.i("Character_Selected", "$character")

        val uID = auth.currentUser?.uid
        val currentCharacter = uID?.let { db.collection(it).document(character.name) }

        val newSpokenValue = !character.spoken
        currentCharacter?.update("spoken", newSpokenValue)
        // 这里不需要调用notifyItemChanged,因为Firebase回调后会自动更新数据并触发Adapter刷新
    }
}

2. 给Adapter添加更新数据的方法,并用DiffUtil高效刷新

修改Adapter,添加updateData方法,使用DiffUtil对比新旧数据,只更新变化的项,避免全量刷新,同时保留滚动位置:

class CharacterAdapter(
    val context : Context,
    private var characters : List<Character>,
    private val itemListener : CharacterItemListener,
    private val recyclerView: RecyclerView 
) : RecyclerView.Adapter<CharacterAdapter.CharacterViewHolder>() {

    // ... ViewHolder和onCreateViewHolder不变

    override fun onBindViewHolder(viewHolder: CharacterViewHolder, position: Int) {
        val character = characters[position]

        with (viewHolder) {
            // ... 图片和文字设置逻辑不变

            // 完全由数据驱动UI状态
            itemView.setBackgroundResource(
                if (character.spoken) R.drawable.background_selected else R.drawable.background
            )

            itemView.setOnClickListener {
                itemListener.characterSelected(character, viewHolder.adapterPosition)
                // 这里不再直接修改character,等待Firebase回调后数据更新再刷新UI
            }
        }
    }

    // 添加更新数据的方法
    fun updateData(newCharacters: List<Character>) {
        val diffResult = DiffUtil.calculateDiff(CharacterDiffCallback(characters, newCharacters))
        characters = newCharacters
        diffResult.dispatchUpdatesTo(this)
    }

    // ... getItemCount和接口不变

    // DiffUtil回调类,对比新旧数据
    private class CharacterDiffCallback(
        private val oldList: List<Character>,
        private val newList: List<Character>
    ) : DiffUtil.Callback() {
        override fun getOldListSize() = oldList.size
        override fun getNewListSize() = newList.size

        override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
            // 用角色名称判断是否是同一个item
            return oldList[oldItemPosition].name == newList[newItemPosition].name
        }

        override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
            // 对比角色的核心字段,判断内容是否变化
            val oldChar = oldList[oldItemPosition]
            val newChar = newList[newItemPosition]
            return oldChar.name == newChar.name && oldChar.spoken == newChar.spoken
        }
    }
}

建议把Character定义为数据类,这样Kotlin会自动生成equals方法,也可以手动实现核心字段的对比逻辑:

data class Character(
    val name: String,
    val spoken: Boolean,
    val type: String // 其他字段
)

为什么这样能解决问题?

  • 复用Adapter实例,避免RecyclerView重新初始化,自然保留滚动位置
  • 使用DiffUtil只刷新变化的item,性能更高,同时不会影响其他item的位置和滚动状态
  • 数据驱动UI,确保UI状态和数据完全同步,避免逻辑不一致

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 10:22:05