构建响应式 UI:用 RxJava 赋能 Android 组件开发
map { frame -> frame.toFloat() / totalFrames } // 计算进度 [0, 1].observeOn(Schedulers.computation()) // 在计算线程执行耗时的Diff计算。.observeOn(AndroidSchedulers.mainThread()) // 回到主线程应用更新。.startWith(ListUpdate(empt
在 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 的操作符(
interval
,concat
,switchMap
)来组合、排序和转换这些流,轻松创建复杂、复合的动画序列。
这种模式的强大之处在于其声明性和组合性。你不再关心“如何”更新 UI(命令式),而是声明“什么”数据会导致“什么”UI 状态(响应式)。这极大地减少了状态同步的 Bug,使代码更加简洁、模块化,并且易于推理和测试。
更多推荐
所有评论(0)