首次使用MVVM架构开发Kotlin应用遭遇IndexOutOfBoundException问题求助
Hey there, let's break down why you're hitting that IndexOutOfBoundException and fix it step by step.
The Root Cause
The core issue here is that all your RecyclerView ViewHolders are sharing the same global tempList, productNamelist, and a single LiveData stream from productViewModel.getObservable(). When you add a new form and search for a product in it, you're clearing and overwriting the shared tempList with new data. If you then try to select a product from an older form, it's trying to access an index that no longer exists in the updated tempList—hence the crash.
Solution 1: Isolate State per ViewHolder (Quick Fix)
First, move the temporary lists and observer logic inside each ViewHolder so every form has its own independent state. We'll also add safety checks for index access to prevent crashes even if something goes wrong.
Here's how to modify your ViewHolder:
class ProductViewHolder(private val binding: ItemProductFormBinding) : RecyclerView.ViewHolder(binding.root) { // Each ViewHolder gets its own temp lists (no more global sharing!) private val tempList = mutableListOf<Product>() private val productNamelist = mutableListOf<String>() fun bind(addProduct: AddProductListener, productViewModel: ProductViewModel, viewLifecycleOwner: LifecycleOwner) { // Clear old observers to prevent cross-form interference productViewModel.getObservable().removeObservers(viewLifecycleOwner) binding.search.doOnTextChanged { text, _, _, _ -> tempList.clear() productNamelist.clear() productViewModel.getProduct( itemView.context, mapOf( "category" to 0, "brand" to 0, "page" to 0, "search" to text!!, "from" to "create" ) ) } productViewModel.getObservable().observe(viewLifecycleOwner, Observer { when (it.status) { AuthResource.AuthStatus.LOADING -> { binding.autoProgress = true } AuthResource.AuthStatus.AUTHENTICATED -> { val list = it.data?.data?.products ?: listOf() productNamelist.clear() list.forEach { li -> productNamelist.add("${li.productName} Brand: ${li.brand?.brandName} \n") } tempList.clear() tempList.addAll(list) binding.search.apply { setAdapter(ArrayAdapter( itemView.context, android.R.layout.simple_spinner_dropdown_item, productNamelist )) } binding.autoProgress = false } AuthResource.AuthStatus.ERROR -> { (itemView.context as? MainActivity)?.session?.toast = it.message ?: "" binding.autoProgress = false } AuthResource.AuthStatus.NOT_AUTHENTICATED -> { binding.autoProgress = false } } }) binding.search.setOnItemClickListener { _, _, pos, _ -> try { // Add safety check for index bounds if (pos in tempList.indices) { setAdapter(tempList[pos], addProduct) Log.e("Search", "Index: ${tempList[pos]}") } } catch (e: Exception) { Log.e("TAG", "bindData: ${e.message}") } } // Add your delete button logic here if not already present } private fun setAdapter(product: Product, addProduct: AddProductListener) { Log.e("Product Index", "Data: $product" ) val varName = mutableListOf<String>() val variant = product.variant ?: listOf() variant.forEach { vari -> varName.add(vari.colorName ?: "NA") } binding.variantSpinner.apply { adapter = ArrayAdapter(context, android.R.layout.simple_spinner_dropdown_item, varName) onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected( parent: AdapterView<*>?, view: View?, position: Int, id: Long ) { // Another safety check for variant index if (position in variant.indices) { val vari = variant[position] Log.e("Variants", "onItemSelected: $vari") binding.cases?.caseProduct?.caseVariant = vari binding.cases?.casePrice = vari.saleAtPrice.toString() binding.cases?.totalAmount = vari.saleAtPrice ?: 0.0 binding.cases?.advanceAmount = vari.saleAtPrice ?: 0.0 binding.cases?.quantity = "1" binding.variants = variant[position] addProduct.onSelect( Soldproduct( 0, binding.cases?.caseId?.toInt(), product.brandId, product.categoryId, product.id, vari.id, vari.buyAtPrice ?: 0.0, vari.saleAtPrice ?: 0.0, 0.0, vari.serialNumber, vari.modelNumber, vari.hsn, vari.sgst.toString(), vari.cgst.toString(), vari.igst.toString(), 1, ) ) binding.invalidateAll() } } override fun onNothingSelected(parent: AdapterView<*>?) { // Optional: Add default state logic here } } } } }
Solution 2: Use Item-Specific ViewModels (Cleaner, Scalable Fix)
For a more robust approach, create a dedicated ViewModel for each RecyclerView item. This ensures complete isolation of state between forms and aligns better with MVVM principles.
Step 1: Create an Item-Level ViewModel
class ItemProductViewModel : ViewModel() { private val _productLiveData = MutableLiveData<AuthResource<ProductResponse>>() val productLiveData: LiveData<AuthResource<ProductResponse>> = _productLiveData fun fetchProducts(context: Context, params: Map<String, Any>) { viewModelScope.launch { try { _productLiveData.postValue(AuthResource.loading()) // Replace with your actual API call logic val response = yourApiService.getProducts(params) _productLiveData.postValue(AuthResource.authenticated(response)) } catch (e: Exception) { _productLiveData.postValue(AuthResource.error(e.message ?: "Failed to load products")) } } } }
Step 2: Update ViewHolder to Use Item ViewModel
class ProductViewHolder(private val binding: ItemProductFormBinding) : RecyclerView.ViewHolder(binding.root) { private lateinit var itemViewModel: ItemProductViewModel private val tempList = mutableListOf<Product>() private val productNamelist = mutableListOf<String>() fun bind(addProduct: AddProductListener, viewLifecycleOwner: LifecycleOwner) { // Get a unique ViewModel instance for this item itemViewModel = ViewModelProvider(itemView.context as ViewModelStoreOwner)[ItemProductViewModel::class.java] binding.search.doOnTextChanged { text, _, _, _ -> tempList.clear() productNamelist.clear() itemViewModel.fetchProducts( itemView.context, mapOf( "category" to 0, "brand" to 0, "page" to 0, "search" to text!!, "from" to "create" ) ) } itemViewModel.productLiveData.observe(viewLifecycleOwner, Observer { // Same state handling logic as before, but using the item's own LiveData when (it.status) { AuthResource.AuthStatus.LOADING -> binding.autoProgress = true AuthResource.AuthStatus.AUTHENTICATED -> { val list = it.data?.data?.products ?: listOf() productNamelist.clear() list.forEach { li -> productNamelist.add("${li.productName} Brand: ${li.brand?.brandName} \n") } tempList.clear() tempList.addAll(list) binding.search.setAdapter( ArrayAdapter( itemView.context, android.R.layout.simple_spinner_dropdown_item, productNamelist ) ) binding.autoProgress = false } AuthResource.AuthStatus.ERROR -> { (itemView.context as? MainActivity)?.session?.toast = it.message ?: "" binding.autoProgress = false } else -> binding.autoProgress = false } }) binding.search.setOnItemClickListener { _, _, pos, _ -> if (pos in tempList.indices) { setAdapter(tempList[pos], addProduct) } } } // Keep your existing setAdapter method here }
Key Takeaways
- Isolate state: Never share mutable lists or LiveData streams between RecyclerView items—each form needs its own data.
- Add safety checks: Always validate index bounds before accessing list elements to prevent crashes.
- Align with MVVM: Item-specific ViewModels make your code more maintainable and scalable as your app grows.
内容的提问来源于stack exchange,提问作者Kepler Staglers

