我们的项目可能在以下场景中用到了RxJava:
- 网络请求(Retrofit)
- 并发任务
- RxView
- 权限请求(RxPermission)
- startActivityForResult(RxImagePicker、RxFilePicker)
我们接下来会讲如何使用kotlin的协程来代替RxJava
网络请求
我们以玩安卓首页banner的接口为例
地址
http service的写法如下,注意返回的类型是Observable
@GET("banner/json")
fun banner(): Observable<BaseJsonListResponse<Banner>>
创建service的时候指定AdapterFactory为RxJava2CallAdapterFactory
val rxJavaService: RxJavaService by lazy {
Retrofit.Builder()
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.baseUrl(baseUrl)
.build().create(RxJavaService::class.java)
}
调用方法如下
val disposable = Instance.rxJavaService
.banner()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
showResult(it, "使用RxJava获取banner")
}
compositeDisposable.add(disposable)
而当我们使用协程的时候,http service的写法就变成了下面这种
@GET("banner/json")
suspend fun banner(): BaseJsonListResponse<Banner>
方法加上suspend关键字
创建service的时候不需要再指定AdapterFactory
val coroutineService: CoroutineService by lazy {
Retrofit.Builder()
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(baseUrl)
.build().create(CoroutineService::class.java)
}
调用方法如下
lifecycleScope.launch {
val result = Instance.coroutineService.banner()
showResult(result, "使用协程获取banner")
}
看着比RxJava更加的直观,毕竟协程就是让你以写同步代码的方式写异步代码
并发任务
在实际的业务场景中,我们可能会遇到要同时并发进行耗时操作的情况,比如上传图片的时候并发压缩所有照片,而不是一个一个顺序压缩
比如我们压缩图片的方法是compressBitmap
private fun compressBitmap(value: Long): Long {
log("在 ${Thread.currentThread().name} 线程中处理")
Thread.sleep(value)
return value
}
我们使用Thread.sleep来模拟耗时操作
现在我们来并发对一个图片数组进行压缩
val bitmapList = listOf<Long>(3000, 3000, 4000)
binding.taskWithRxjava.setOnClickListener {
val start = System.currentTimeMillis()
Observable.zip(bitmapList.map {
Observable
.just(it)
.observeOn(Schedulers.io())
.map { value ->
compressBitmap(value)
}
}) {
it
}
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
AlertDialog.Builder(this)
.setTitle("获取到结果 任务耗时 ${System.currentTimeMillis() - start}毫秒")
.setMessage(it.toString())
.setPositiveButton("确定", null)
.show()
}.addTo(compositeDisposable)
}
我们同时压缩3张图片,大概4秒后,我们可以看到一个提示成功的弹窗,再看log日志,可以看出三个压缩任务在不同的线程执行
2021-08-17 09:57:30.463 11705-11751/com.ke.rxjava_coroutine D/TAG: 在 RxCachedThreadScheduler-3 线程中处理
2021-08-17 09:57:30.463 11705-11749/com.ke.rxjava_coroutine D/TAG: 在 RxCachedThreadScheduler-1 线程中处理
2021-08-17 09:57:30.463 11705-11750/com.ke.rxjava_coroutine D/TAG: 在 RxCachedThreadScheduler-2 线程中处理
现在我们使用协程来进行并发耗时操作
代码如下
lifecycleScope.launch {
val start = System.currentTimeMillis()
val result = bitmapList.map {
GlobalScope.async {
compress(it)
}
}
.map {
it.await()
}
AlertDialog.Builder(this@MainActivity)
.setTitle("协程并发 获取到结果 任务耗时 ${System.currentTimeMillis() - start}毫秒")
.setMessage(result.toString())
.setPositiveButton("确定", null)
.show()
}
同样,大约4秒后,我们看到弹窗提示处理完成,同时通过日志也能看出三个任务在不同线程中进行
2021-08-17 10:09:56.081 13978-14300/com.ke.rxjava_coroutine D/TAG: 在 DefaultDispatcher-worker-1 线程中处理
2021-08-17 10:09:56.081 13978-14301/com.ke.rxjava_coroutine D/TAG: 在 DefaultDispatcher-worker-2 线程中处理
2021-08-17 10:09:56.082 13978-14302/com.ke.rxjava_coroutine D/TAG: 在 DefaultDispatcher-worker-3 线程中处理
RxView
RxView把View的事件(点击,选中状态改变,文字改变)都变成了Observable,我们可以以处理流的形式处理View的事件,这里主要讨论两个场景
- 防止重复点击
使用RxJava处理方式如下
binding.clickWithRxjava.clicks()
.throttleFirst(5, java.util.concurrent.TimeUnit.SECONDS)
.subscribe {
log("点击了按钮")
}
加了throttleFirst后,订阅到的点击事件在不停点击按钮后每5秒只会触发一次
使用flow可以达到类似的效果
@ExperimentalCoroutinesApi
fun View.clickEventFlow(): Flow<Unit> {
var lastClickTime = System.currentTimeMillis()
return callbackFlow {
setOnClickListener {
if (System.currentTimeMillis() - lastClickTime > 5000) {
lastClickTime = System.currentTimeMillis()
offer(Unit)
}
}
awaitClose {
setOnClickListener(null)
}
}
}
- 优化搜索
大致意思是如果用户输入的是123456789,我们希望只发出一次123456789网络请求,而不是输入一个1发一次网络请求,输入一个2发出12的网络请求
使用RxView处理方式如下
binding.etContent.textChanges().debounce(1, TimeUnit.SECONDS)
.subscribe {
log("输入框信息发生变化 来自RxJava $it")
}
在停止输入一秒后才发起网络请求,避免不必要的网络请求
我们也可以使用Flow来实现这个功能。首先把TextView的文字变化转换成Flow
@ExperimentalCoroutinesApi
fun TextView.textChangeFlow(): Flow<CharSequence> {
return callbackFlow {
val watcher = object : TextWatcher {
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
}
override fun onTextChanged(content: CharSequence?, p1: Int, p2: Int, p3: Int) {
if (content != null) {
offer(content)
}
}
override fun afterTextChanged(p0: Editable?) {
}
}
addTextChangedListener(watcher)
awaitClose {
removeTextChangedListener(watcher)
}
}
}
然后同样需要加上debounce
lifecycleScope.launch {
binding.etContent.textChangeFlow().debounce(1000)
.collect {
log("输入框信息发生变化 来自Flow $it")
}
}
权限请求
不使用任何库请求权限的方式如下:
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), 422
)
在onRequestPermissionsResult回调方法里判断用户是否授权
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if(requestCode == 422){
}
}
使用RxPermissions请求就看起来方便很多,代码如下
RxPermissions(this)
.request(Manifest.permission.ACCESS_FINE_LOCATION)
.subscribe {
log("使用RxJava请求权限结果 $it")
}
RxPermissions的原理是创建一个新的Fragment去请求权限,我们也可以利用这点建立一个不可见的Fragment并用它去请求权限和处理回调。
Github上已经有一个现成的框架,地址
不用协程,仅用Kotlin的方法参数就可以实现。
startActivityForResult(RxImagePicker、RxFilePicker)
和上面说的一样,创建一个不可见的fragment来启动activity和处理回调
结尾
现在,可以放弃RxJava了吗