RxBinding
如今的Android开发中越来越多地开始引进MVI、Redux、单向数据流等概念,力求实现像react等前端框架那样的响应式UI开发体验。
除了彻底转向Jetpack Compose那样的激进方案外,客户端也有一些因地制宜的方案,比如RxBinding,通过RxJava与Android View的配合,用Observable替代OnClickListener,从而更高效地实现基于事件驱动的UI开发。
findViewById<Button>(R.id.button).clicks().subscribe {
// handle button clicked
}
FlowBinding
kotlinx.coroutines 从1.3 开始增加了Flow库,可以在CoroutineScope中响应式地处理流式数据,堪称协程版的RxJava。相应的出现了很多RxJava=>Flow的优秀项目,今天介绍的FlowBinding就是其中一个:Flow版的RxBinding。
// Platform bindings
implementation "io.github.reactivecircus.flowbinding:flowbinding-android:${flowbinding_version}"
// AndroidX bindings
implementation "io.github.reactivecircus.flowbinding:flowbinding-appcompat:${flowbinding_version}"
implementation "io.github.reactivecircus.flowbinding:flowbinding-core:${flowbinding_version}"
implementation "io.github.reactivecircus.flowbinding:flowbinding-drawerlayout:${flowbinding_version}"
implementation "io.github.reactivecircus.flowbinding:flowbinding-navigation:${flowbinding_version}"
implementation "io.github.reactivecircus.flowbinding:flowbinding-recyclerview:${flowbinding_version}"
implementation "io.github.reactivecircus.flowbinding:flowbinding-swiperefreshlayout:${flowbinding_version}"
implementation "io.github.reactivecircus.flowbinding:flowbinding-viewpager2:${flowbinding_version}"
// Material Components bindings
implementation "io.github.reactivecircus.flowbinding:flowbinding-material:${flowbinding_version}"
除了Android标准控件以为,FlowBinding也支持各种AndroidX中的控件以及Material控件
使用方法
在CoroutineScope中监听OnClick事件:
uiScope.launch {
findViewById<Button>(R.id.button)
.clicks() // this returns a Flow<Unit>
.collect {
// handle button clicked
}
}
kotlinx-coroutines-core
中提供了launchIn(scope)
,可以简化 scope.launch { flow.collect() }
的写法:
findViewById<Button>(R.id.button)
.clicks() // binding API available in flowbinding-android
.onEach {
// handle button clicked
}
.launchIn(uiScope)
上面例子中的uiScope代表与Activity/Fragment的Lifecycle一致CoroutineScope,有效避免内存泄漏。
实际开发中可以使用androidx.lifecycle:lifecycle-runtime-ktx:2.2.0
提供的扩展属性LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
,可以在lifecycle的onDestroy的时候,取消其Scope内的协程:
class ExampleActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_example)
findViewById<Button>(R.id.button)
.clicks()
.onEach {
// handle button clicked
}
.launchIn(lifecycleScope) // provided by lifecycle-runtime-ktx
}
}
实现原理
实现原理比较简单:
scope.launch {
findViewById<Button>(R.id.button)
.clicks() // this returns a Flow<Unit>
.collect {
// handle button clicked
}
}
通过callbackFlow
,可以将一个callback转成Flow,实现上面的clicks()
方法
fun View.clicks(): Flow<Unit> = callbackFlow
val listener = View.OnClickListener {
offer(Unit)
}
setOnClickListener(listener)
awaitClose { setOnClickListener(null) }
}
awaitClose{}
会在flow结束时执行,所以可以在此处进行反注册
offer()
将数据发射到Flow内部使用的SendChannel
,但是如果Flow已关闭,可能会抛出异常,所以可以增加对异常的捕获处理
fun <E> SendChannel<E>.safeOffer(value: E) = !isClosedForSend && try {
offer(value)
} catch (e: CancellationException) {
false
}
整段代码如下:
最后
FlowBinding借助Coroutine Flow打造了一套响应式UI的工具库,配合LifecycleScope实现自动注销避免内存泄漏。在Kotlin大行其道的今天,建议使用FlowBinding替代RxBinding,并以此为契机发掘更多地使用Flow替代RxJava的使用场景。