回调在安卓编程无所不在。即使你不想多并发编程,在为UI组件编写响应函数时,也会遇到回调listener。回调使得代码可读性差,增大了代码的维护难度。这时你就会想念Windows的事件编程自动替你把回调函数变成普通函数的好处。如果你熟悉Go语言,在编写安卓的多并发程序时,回调地狱也会使你对Go的多并发设计肃然起敬。
幸好现在Kotlin语言的引入,很大程度上解决了使用Java存在的回调地狱问题。虽然我认为还没达到Go语言的如丝般顺滑,但已不会再陷入可怕的回调地狱了。
先说一下与协程无关的,优化UI中回调的简单方法。对于UI响应函数,一般是直接写在Activity的onCreat或Fragment的onCreateView里。这对于代码很短的响应函数就比较适合,但对于代码较长的响应函数,写在onCreate函数里,导致onCreate函数代码的可读性就很差了。这时,建议增加一个变量,将该响应函数挪到OnCeate函数之外,作为Activity的一个单独函数。
okButton.setOnClickListener { ..... //处理逻辑 Toast.makeText(activity, "You clicked me.", Toast.LENGTH_SHORT).show() }
对于如上代码,可以修改为
okButton.setOnClickListener(okClickListener) ...... } //onCreate 结束 private val okClickListener = View.OnClickListener { ..... //处理逻辑 Toast.makeText(activity, "You clicked me.", Toast.LENGTH_SHORT).show() }
经过这样改动,onCreate的代码可读性就好了很多。
下面回到正题,如何在Kotlin里使用协程优雅地解决回调地狱。
安卓要求可能费时引起阻塞的操作(如网络访问)都不能运行在主线程。这时传统的方法就是另起一个线程来执行这些操作。但是带来一个问题就是主程序如何知道其操作结束,特别是主程序需要等待其操作结果,以便决定下一步动作时,问题更是突出。这时回调就出现了,可以在主程序设置该操作出错或有结果时回调。
这里引用《第一行代码Android》里sendHttpRequest的例子。
//定义回调函数接口 interface HttpCallbackListener{ fun onFinish(response:String) fun onError(e:Exception) } fun sendHttpRequest(address:String, listener:HttpCallbackListener) { thread{ try{ ......//处理逻辑 //回调onFinish listener.onFinish(response.toString()) }catch(e:Exception) { //回调onError listener.onError(e) } } } //这时,使用这个函数sendHttpRequest,就需要带回调函数了 sendHttpRequest(address, object:HttpCallbackListener { override fun onFinish(response:String) { //处理逻辑 } override fun onError(e:Exception) { //处理逻辑 } })
如果一段代码,需要在onFinish里多次嵌套回调函数,那就陷入了可怕的回调地狱,不但代码可读性极差,编写过程起来也非常痛苦。现在请出协程来解决。《第一行代码Android》里,对于这个例子是进行简单改造,将回调函数包装到一个新的函数request里,这样底层仍存在回调,没有充分利用协程特性,并不彻底。
suspend fun sendHttpRequest(address:String) : String{ try{ ......//处理逻辑 //无需回调直接return return response }catch(e:Exception) { //无需回调onError //如果留待外部处理错误就继续抛出例外, 如果在这里处理消化,就编写处理逻辑不再抛出例外 throw e } } } //下面是调用示例 var netJob=Job() CoroutineScope(netJob).launch { try { var response=sendHttpRequest(address) //处理逻辑 ...... }catch(e:Exception) { //处理错误 } }
看上述代码,已经完全不需要回调函数了,这是区别于《第一行代码Android》里示例的显著特点。整个代码是顺序执行,符合人的思维模式,编程相对容易,代码也易维护。
这里出现了两个关键字需要重点解释一下:
1、suspend 用于修饰fun,表明这个函数是可挂起的函数,它可以被中途暂停、恢复,甚至未执行完就被取消。这样的函数只能被另一个可挂起的函数调用,或者在协程作用域内调用。
2、CoroutineScope 协程作用域,与Job配合,可以自定义协程的作用范围(自主操作这些协程的状态)。在 CoroutineScope(netJob).launch 后,就可以像普通函数一样调用挂起函数。
实践中,可以将上述netJob 定义为类成员变量,然后在该类的资源释放环节调用Job.cancel(),终止该作用域下的所有协程。
使用上述方法就可以与非UI部分的回调函数说bye-bye了。关于安卓Kotlin协程的更多用法,以后再详述。