RecyclerView滚动时Item值重置问题排查及解决方案咨询
Hey, 我看了你的代码,马上就发现问题所在了——你没把用户输入的EditText值同步到Adapter的数据源里,反而存在ViewHolder的savedStock里了!RecyclerView的ViewHolder是会被复用的,滚动的时候旧的ViewHolder会被拿来绑定新位置的数据,你存在ViewHolder里的临时数据自然就没了,导致值重置。完全不用换成ListView,RecyclerView的复用机制是它的优势,只要改对数据维护的逻辑就行,下面是具体的解决方案:
核心问题分析
你的StockFragmentHolder里维护了一个savedStock对象,填充UI和处理点击时都用这个临时对象,而不是更新Adapter的listStocks数据源。当RecyclerView滚动复用ViewHolder时,这个savedStock会被新的位置数据覆盖,之前用户输入的值就丢失了。
具体修复步骤
1. 移除ViewHolder中的临时savedStock,所有数据操作直接基于Adapter的数据源
ViewHolder只负责UI绑定和事件监听,不应该存储数据,数据统一由Adapter的listStocks管理。
2. 给EditText添加文本监听,实时更新数据源
用户输入时,立刻把输入值同步到listStocks对应的Stock对象里,这样滚动时重新绑定数据就能拿到最新值。
3. 修正populateView方法,直接从数据源获取数据填充UI
不再依赖ViewHolder的临时对象,直接用传入的stock参数(也就是数据源里的对象)来填充EditText和其他控件。
4. 修复Adapter中的几个语法错误
onCreateViewHolder没有返回ViewHolder实例onBindViewHolder需要把RecyclerView.ViewHolder强转成你的StockFragmentHolder
修改后的完整代码
StocksAdapter 修正版
class StocksAdapter() : RecyclerView.Adapter<StocksAdapter.StockFragmentHolder>() { // 扩展函数可以放在类外面或者companion object里,避免ViewHolder混乱 fun Double.format(digits: Int) = java.lang.String.format("%,.${digits}f", this) inner class StockFragmentHolder(view: View) : RecyclerView.ViewHolder(view) { lateinit var numberOfShares: EditText lateinit var buyPrice: EditText lateinit var btnCompute: Button lateinit var btnRemove: Button lateinit var stockAveragePrice: TextView lateinit var stockTotalAmount: TextView init { numberOfShares = view.findViewById(R.id.stockNumberOfShares) buyPrice = view.findViewById(R.id.stockBuyPrice) btnCompute = view.findViewById(R.id.btnCompute) btnRemove = view.findViewById(R.id.btnRemove) stockAveragePrice = view.findViewById(R.id.stockAveragePrice) stockTotalAmount = view.findViewById(R.id.stockTotalAmount) } fun populateView(stock: Stock, onStocksAdapterListener: OnStocksAdapterListener, position: Int) { // 直接用数据源的stock对象填充UI buyPrice.setText(if (stock.buyPrice <= 0.0) "" else stock.buyPrice.toString()) numberOfShares.setText(if (stock.numberOfShares <= 0L) "" else stock.numberOfShares.toString()) stockAveragePrice.text = stock.averagePrice?.format(2) ?: "0" stockTotalAmount.text = stock.totalAmount?.format(2) ?: "0" // 监听EditText输入,实时更新数据源 numberOfShares.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} override fun afterTextChanged(s: Editable?) { stock.numberOfShares = s?.toString()?.toLongOrNull() ?: 0L } }) buyPrice.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} override fun afterTextChanged(s: Editable?) { stock.buyPrice = s?.toString()?.toDoubleOrNull() ?: 0.0 } }) btnCompute.setOnClickListener { val currentStock = stock if (currentStock.numberOfShares > 0L) { val buyTotalAmount = StocksCalculator.calculateTotalSharesPrice( currentStock, StocksCalculator.TRANSACTION_FEE_BASE_VALUES, Constants.TRANSACTION_TYPE.TRANSACTION_TYPE_BUY ) val averagePricePerShare = buyTotalAmount / currentStock.numberOfShares // 计算后更新数据源的字段,同时更新UI currentStock.averagePrice = averagePricePerShare currentStock.totalAmount = buyTotalAmount stockAveragePrice.text = averagePricePerShare.format(2) stockTotalAmount.text = buyTotalAmount.format(2) onStocksAdapterListener.onStocksComputeButtonClicked( currentStock.numberOfShares, averagePricePerShare, buyTotalAmount, position ) } } btnRemove.setOnClickListener { onStocksAdapterListener.onStocksRemoveButtonClicked(position) } } } lateinit var onStocksAdapterListener: OnStocksAdapterListener lateinit var listStocks: ArrayList<Stock> constructor(listStocks: ArrayList<Stock>, onStocksAdapterListener: OnStocksAdapterListener) : this() { this.listStocks = listStocks this.onStocksAdapterListener = onStocksAdapterListener setHasStableIds(true) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StockFragmentHolder { val itemView = LayoutInflater.from(parent.context) .inflate(R.layout.fragment_stock, parent, false) return StockFragmentHolder(itemView) // 修复:返回ViewHolder实例 } override fun getItemViewType(position: Int): Int { return position } override fun getItemCount(): Int { return listStocks.size } override fun onBindViewHolder(holder: StockFragmentHolder, position: Int) { val stock = listStocks[position] holder.populateView(stock, this.onStocksAdapterListener, position) } interface OnStocksAdapterListener { fun onStocksAddButtonClicked() fun onStocksComputeButtonClicked( numberOfShares: Long?, averagePricePerShare: Double?, averageTotalAmount: Double?, position: Int ) fun onStocksRemoveButtonClicked(position: Int) } }
补充说明:Stock类需要添加额外字段
为了保存计算后的均价和总额,你的Stock类需要新增两个字段:
data class Stock( var numberOfShares: Long = 0L, var buyPrice: Double = 0.0, var sellPrice: Double = 0.0, var averagePrice: Double? = null, // 新增:保存计算后的均价 var totalAmount: Double? = null // 新增:保存计算后的总额 )
为什么禁用setIsRecyclable(false)没用?
这个方法只是禁止ViewHolder被复用,滚动时会创建新的ViewHolder,但你依然没有把用户输入的值保存到数据源里,所以当滚动回来时,还是会从原始的listStocks里拿旧数据填充,自然还是会重置。只有把数据同步到数据源,才能保证滚动时显示的是最新值。
内容的提问来源于stack exchange,提问作者Kenzo-kun

