Kotlin协程解决安卓回调地狱

回调在安卓编程无所不在。即使你不想多并发编程,在为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协程的更多用法,以后再详述。

发表评论