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

基于RxAndroid实现选中商品价格实时更新的技术方案咨询

嘿,这个需求用RxAndroid来实现其实挺顺畅的,核心就是把商品的状态变化转化为可观察的数据流,然后通过合并这些流来实时计算总价。我给你一步步拆解实现方案:

核心思路

我们要把每个商品的选中状态和数量变化都做成可观察的事件源,当用户操作商品时,这些事件源会发射新的状态。然后我们合并所有商品的状态流,每次有状态变化时就重新计算选中商品的总价和总数量,最后把结果实时更新到TextView上。

步骤1:定义带Observable状态的商品数据类

每个商品对象需要用BehaviorSubject来保存选中状态和数量——它的好处是既能保存当前状态,新订阅者也能立即拿到当前值,完美适配实时更新的场景:

data class Product(
    val id: String,
    val name: String,
    val price: Double,
    // 默认未选中
    private val isSelectedSubject: BehaviorSubject<Boolean> = BehaviorSubject.createDefault(false),
    // 默认数量为1
    private val quantitySubject: BehaviorSubject<Int> = BehaviorSubject.createDefault(1)
) {
    // 对外只暴露Observable,防止外部随意修改状态
    val isSelectedObservable: Observable<Boolean> get() = isSelectedSubject
    val quantityObservable: Observable<Int> get() = quantitySubject

    // 切换选中状态
    fun toggleSelected() {
        val current = isSelectedSubject.value ?: false
        isSelectedSubject.onNext(!current)
    }

    // 增加数量
    fun increaseQuantity() {
        val current = quantitySubject.value ?: 1
        quantitySubject.onNext(current + 1)
    }

    // 减少数量(最少保留1个)
    fun decreaseQuantity() {
        val current = quantitySubject.value ?: 1
        if (current > 1) {
            quantitySubject.onNext(current - 1)
        }
    }

    // 取消选中(可选重置数量为1)
    fun deselect() {
        isSelectedSubject.onNext(false)
        quantitySubject.onNext(1)
    }
}

步骤2:在RecyclerView Adapter中处理状态交互

Adapter需要做两件事:一是把用户的操作(点击选中、增减数量)转发给商品对象;二是收集所有商品的状态流,供外部合并计算。同时要注意订阅的解绑,避免内存泄漏:

class ProductAdapter(private val products: List<Product>) : RecyclerView.Adapter<ProductAdapter.ProductViewHolder>() {

    // 暴露所有商品的状态合并流
    fun getProductStateObservables(): List<Observable<Pair<Boolean, Int>>> {
        return products.map { product ->
            // 合并选中状态和数量的变化,任一变化就发射最新的配对值
            Observable.combineLatest(
                product.isSelectedObservable,
                product.quantityObservable
            ) { isSelected, quantity -> Pair(isSelected, quantity) }
        }
    }

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

    override fun onBindViewHolder(holder: ProductViewHolder, position: Int) {
        holder.bind(products[position])
    }

    override fun getItemCount() = products.size

    inner class ProductViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        private val tvName = itemView.findViewById<TextView>(R.id.tv_name)
        private val tvPrice = itemView.findViewById<TextView>(R.id.tv_price)
        private val cbSelected = itemView.findViewById<CheckBox>(R.id.cb_selected)
        private val btnMinus = itemView.findViewById<Button>(R.id.btn_minus)
        private val tvQuantity = itemView.findViewById<TextView>(R.id.tv_quantity)
        private val btnPlus = itemView.findViewById<Button>(R.id.btn_plus)
        private val quantityLayout = itemView.findViewById<LinearLayout>(R.id.layout_quantity)

        fun bind(product: Product) {
            // 初始化UI显示
            tvName.text = product.name
            tvPrice.text = "$${product.price}"
            cbSelected.isChecked = product.isSelectedObservable.value ?: false
            tvQuantity.text = product.quantityObservable.value.toString()
            quantityLayout.visibility = if (cbSelected.isChecked) View.VISIBLE else View.GONE

            // 处理选中事件
            cbSelected.setOnCheckedChangeListener { _, isChecked ->
                if (isChecked) product.toggleSelected() else product.deselect()
            }

            // 处理数量增减
            btnPlus.setOnClickListener { product.increaseQuantity() }
            btnMinus.setOnClickListener { product.decreaseQuantity() }

            // 订阅商品状态变化,实时更新Item UI
            product.isSelectedObservable.subscribe { isSelected ->
                cbSelected.isChecked = isSelected
                quantityLayout.visibility = if (isSelected) View.VISIBLE else View.GONE
            }.disposeOnViewDetachedFromWindow(itemView)

            product.quantityObservable.subscribe { quantity ->
                tvQuantity.text = quantity.toString()
            }.disposeOnViewDetachedFromWindow(itemView)
        }
    }
}

这里用disposeOnViewDetachedFromWindow可以在Item从屏幕消失时自动解绑订阅,避免内存泄漏。

步骤3:在Activity/Fragment中合并流计算总价

把所有商品的状态流合并,每次有状态变化就重新计算总价和总数量,最后更新到TextView:

class MainActivity : AppCompatActivity() {
    private lateinit var adapter: ProductAdapter
    private lateinit var tvTotalPrice: TextView
    private lateinit var tvTotalQuantity: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 初始化示例商品列表
        val products = listOf(
            Product("1", "商品A", 19.99),
            Product("2", "商品B", 29.99),
            Product("3", "商品C", 9.99)
        )

        // 初始化RecyclerView
        val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
        adapter = ProductAdapter(products)
        recyclerView.adapter = adapter
        recyclerView.layoutManager = LinearLayoutManager(this)

        tvTotalPrice = findViewById(R.id.tv_total_price)
        tvTotalQuantity = findViewById(R.id.tv_total_quantity)

        // 合并所有商品的状态流,实时计算总价
        val totalInfoObservable = Observable.merge(adapter.getProductStateObservables())
            // 每次状态变化时重新计算
            .map { calculateTotal(products) }
            // 页面初始化时先发射一次初始计算结果
            .startWith(calculateTotal(products))
            // 切换到主线程更新UI
            .observeOn(AndroidSchedulers.mainThread())

        // 订阅结果流,更新TextView
        totalInfoObservable.subscribe { (totalPrice, totalQuantity) ->
            tvTotalPrice.text = "总价: $%.2f".format(totalPrice)
            tvTotalQuantity.text = "总数量: $totalQuantity"
        }.disposeOnDestroy(this)
    }

    // 计算选中商品的总价和总数量
    private fun calculateTotal(products: List<Product>): Pair<Double, Int> {
        var totalPrice = 0.0
        var totalQuantity = 0
        products.forEach { product ->
            val isSelected = product.isSelectedObservable.value ?: false
            val quantity = product.quantityObservable.value ?: 1
            if (isSelected) {
                totalPrice += product.price * quantity
                totalQuantity += quantity
            }
        }
        return Pair(totalPrice, totalQuantity)
    }
}

这里用disposeOnDestroy确保Activity销毁时解绑订阅,防止内存泄漏。

关键注意点

  • BehaviorSubject的选择:它能保存当前状态,新订阅者立即拿到最新值,非常适合这种需要实时同步状态的场景。
  • 线程切换:如果商品数量极大,计算总价的逻辑可以放到IO线程(用subscribeOn(Schedulers.io())),再切回主线程更新UI。
  • 内存泄漏防护:一定要用RxAndroid的扩展方法自动解绑订阅,避免因订阅持有Activity引用导致内存泄漏。

这样用户每次操作商品(选中/取消、增减数量),都会触发状态流的变化,进而自动重新计算并更新总价TextView,完全满足实时更新的需求。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.26 09:36:06