在 Android 开发中,UI 是数据变化的直观反映。传统的命令式编程(setText()setVisibility()notifyDataSetChanged())要求开发者手动追踪数据状态并在正确的地方调用正确的方法,这个过程繁琐且容易出错。

RxJava 的响应式范式将这种关系颠倒过来。我们不再命令 UI 如何改变,而是声明数据流(Observable)与 UI 状态(View 属性)之间的绑定关系。当数据流发射新值时,UI 会自动响应更新。本文将探索如何用这种思想构建自定义 View、实现列表增量更新和控制复杂动画

一、核心思想:从命令式到声明式

命令式 (Imperative):

kotlin

// 传统方式:需要手动调用
fun onDataLoaded(newData: List<Item>) {
    if (newData.isEmpty()) {
        emptyStateView.visibility = View.VISIBLE
        recyclerView.visibility = View.GONE
    } else {
        emptyStateView.visibility = View.GONE
        recyclerView.visibility = View.VISIBLE
        adapter.data = newData
        adapter.notifyDataSetChanged() // 全量更新,性能低下
    }
}

响应式 (Declarative with RxJava):

kotlin

// ViewModel
val uiState: Observable<UiState> = dataRepository.getItems()
    .map { items -> 
        if (items.isEmpty()) UiState.Empty 
        else UiState.Data(items)
    }
    .startWith(UiState.Loading)

// UI 层:只需声明一次绑定
viewModel.uiState
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe { state ->
        when (state) {
            is UiState.Loading -> showLoading()
            is UiState.Data -> showData(state.items) // 内部处理高效更新
            is UiState.Empty -> showEmptyState()
        }
    }

二、自定义响应式 View 组件

我们可以创建自带数据流的 View,使其能够直接对外部数据的变化做出反应。

场景:创建一个显示实时评分(0-5星)的 RatingStatsView

1. 定义自定义 View

kotlin

class RatingStatsView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    private var rating: Float = 0f
    private var paint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.YELLOW
    }

    // 传统的 setter 方法
    fun setRating(r: Float) {
        rating = r.coerceIn(0f, 5f)
        invalidate() // 请求重绘
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        // 根据 rating 绘制星星
        drawStars(canvas, rating)
    }
}

2. 为其添加响应式扩展
我们不直接修改原始 View,而是通过 Kotlin 扩展函数为其添加响应式能力。这是更灵活和推荐的做法。

kotlin

// 创建一个扩展函数,将 View 的某个属性与一个 Observable 绑定
fun RatingStatsView.bindRating(observable: Observable<Float>) {
    // 管理订阅生命周期至关重要!
    val disposable = observable
        .distinctUntilChanged() // 避免重复设置相同的值
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe { newRating ->
            this.setRating(newRating) // 调用原有的命令式方法
        }
    
    // 关键:将 Disposable 的生命周期与 View 的附着窗口状态绑定
    // 使用 RxLifecycle 或 android-ktx 的 View.onDestroy() 扩展
    this.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
        override fun onViewAttachedToWindow(v: View?) {
            // 通常在 onViewAttachedToWindow 时自动处理,这里主要处理销毁
        }

        override fun onViewDetachedFromWindow(v: View?) {
            disposable.dispose() // View 从窗口分离时,自动取消订阅,防止内存泄漏
        }
    })
}

3. 在 Activity/Fragment 中使用

kotlin

// ViewModel 中有一个流,实时计算产品的平均评分
val productRating: Observable<Float> = ...

// 在 UI 层,一行代码建立绑定
ratingStatsView.bindRating(viewModel.productRating)

现在,只要 productRating 流发射新的评分值,RatingStatsView 就会自动更新UI。所有线程调度和生命周期管理都被封装了起来。


三、列表数据的增量更新 (DiffUtil + RxJava)

RecyclerView 的全量更新 (notifyDataSetChanged()) 性能很差。DiffUtil 可以计算新旧数据集的差异,实现高效的动画更新。将它与 RxJava 结合是天作之合。

1. 在 ViewModel 中准备数据流

kotlin

class ListViewModel {

    // 数据源流:可能来自数据库、网络,并可能被搜索、过滤等操作转换
    private val rawData: Observable<List<Item>> = ...

    // 使用操作符处理数据,准备进行Diff
    val listItems: Observable<List<Item>> = rawData
        .observeOn(Schedulers.computation()) // 在计算线程执行耗时的Diff计算
        .scan { previousList, newList ->
            // scan 操作符类似于 fold,但它会发射每一次中间结果
            // 这是关键:我们持有上一次的列表,用于和新的列表做对比
            val diffResult = DiffUtil.calculateDiff(
                object : DiffUtil.Callback() {
                    override fun getOldListSize(): Int = previousList.size
                    override fun getNewListSize(): Int = newList.size
                    override fun areItemsTheSame(oldPos: Int, newPos: Int): Boolean =
                        previousList[oldPos].id == newList[newPos].id
                    override fun areContentsTheSame(oldPos: Int, newPos: Int): Boolean =
                        previousList[oldPos] == newList[newPos]
                }
            )
            // 我们需要发射一个包裹,里面包含新数据和Diff结果
            // 自定义一个 sealed class 来承载
            ListUpdate(newList, diffResult)
        }
        .startWith(ListUpdate(emptyList(), null)) // 初始状态
        .observeOn(AndroidSchedulers.mainThread()) // 回到主线程应用更新
}

2. 定义数据更新包裹类

kotlin

sealed class ListUpdate<out T>(val list: List<T>, val diffResult: DiffResult?) {
    class Initial<T>(list: List<T>) : ListUpdate<T>(list, null)
    class Differential<T>(list: List<T>, diffResult: DiffResult) : ListUpdate<T>(list, diffResult)
}

3. 在 UI 层订阅并应用更新

kotlin

// 在 Activity/Fragment 中
viewModel.listItems
    .subscribe { listUpdate ->
        when (listUpdate) {
            is ListUpdate.Initial -> {
                // 初次加载,直接设置数据
                adapter.submitList(listUpdate.list)
            }
            is ListUpdate.Differential -> {
                // 增量更新:使用DiffResult分发更新,带动画
                listUpdate.diffResult.dispatchUpdatesTo(adapter)
                // 注意:对于 ListAdapter 或 AsyncListDiffer,你可能需要其他方式,
                // 但原理相同:在子线程计算Diff,在主线程应用结果。
            }
        }
    }.addTo(compositeDisposable)

这种方法将耗时的 DiffUtil 计算完全移出了主线程,保证了 UI 的流畅性,同时获得了高效的动画更新。


四、动画的响应式控制

复杂动画通常涉及多个值在不同时间点的变化。RxJava 的 Observable 是描述时间序列的完美工具。

场景:实现一个按钮,点击后有一个颜色脉冲动画,然后平滑移动到屏幕中央。

1. 使用 RxJava 生成动画值流

kotlin

// 这是一个工具函数,生成一个在 duration 内从 0f 到 1f 的浮点数流
fun createAnimationStream(durationMs: Long): Observable<Float> {
    val totalFrames = (durationMs / 16).toInt() // ~60fps
    return Observable.interval(16, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
        .take(totalFrames.toLong())
        .map { frame -> frame.toFloat() / totalFrames } // 计算进度 [0, 1]
        .concatWith(Observable.just(1f)) // 确保以1结束
        .startWith(0f) // 以0开始
}

2. 组合多个动画流

kotlin

// 点击事件流
val pulseClicks: Observable<Unit> = RxView.clicks(button)

// 响应点击,触发动画序列
pulseClicks
    .switchMap { click -> 
        // 第一个动画:颜色脉冲(使用 switchMap 确保每次点击都会取消之前的未完成动画)
        val pulseAnim = createAnimationStream(300)
            .map { fraction -> 
                // 使用估值器(ArgbEvaluator)计算中间颜色
                val startColor = ContextCompat.getColor(context, R.color.normal)
                val endColor = ContextCompat.getColor(context, R.color.highlight)
                evaluateColor(fraction, startColor, endColor)
            }

        // 第二个动画:移动(在脉冲完成后开始)
        val moveAnim = Observable.timer(300, TimeUnit.MILLISECONDS) // 延迟300ms
            .flatMap { 
                createAnimationStream(500).map { fraction ->
                    // 计算目标位置
                    val startX = button.x
                    val targetX = (screenWidth - button.width) / 2f
                    startX + (targetX - startX) * fraction
                }
            }

        // 合并两个动画流:脉冲阶段发射颜色,移动阶段发射位置
        Observable.merge(
            pulseAnim.map { value -> AnimValue.Color(value) },
            moveAnim.map { value -> AnimValue.PositionX(value) }
        )
    }
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe { animValue ->
        // 根据接收到的值应用动画
        when (animValue) {
            is AnimValue.Color -> button.setBackgroundColor(animValue.value)
            is AnimValue.PositionX -> button.x = animValue.value
        }
    }.addTo(compositeDisposable)

3. 定义动画值密封类

kotlin

sealed class AnimValue {
    data class Color(val value: Int) : AnimValue()
    data class PositionX(val value: Float) : AnimValue()
}

总结

通过 RxJava,我们将 UI 开发提升到了一个全新的抽象层次:

  • 自定义组件: 通过扩展函数为任何 View 注入响应式能力,使其能够自动响应数据流。

  • 列表更新: 将昂贵的 DiffUtil 计算卸载到后台线程,并通过流式编程优雅地处理数据序列,实现高效、动画化的增量更新。

  • 动画控制: 将动画视为时间序列的值流,使用 RxJava 的操作符(intervalconcatswitchMap)来组合、排序和转换这些流,轻松创建复杂、复合的动画序列。

这种模式的强大之处在于其声明性组合性。你不再关心“如何”更新 UI(命令式),而是声明“什么”数据会导致“什么”UI 状态(响应式)。这极大地减少了状态同步的 Bug,使代码更加简洁、模块化,并且易于推理和测试。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐