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

首次使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.30 18:47:48