用Kotlin协程消灭安卓开发中的回调地狱
原创 Pony 拍码场
安卓原生开发的痛点
自Android平台推出以来,Java一直是开发Android应用的主要语言。尽管后来Kotlin成为了谷歌主推的编程语言,Java仍然被广泛使用,在Android开发中仍占有重要地位。从github的数据看,用Java写的安卓项目仍然是Kotlin的2倍以上;我们的项目创建较早,大部分代码是用Java编写的,当我们用Java处理异步任务的时候,容易陷入回调地狱,下面用伪代码给出一个例子:
login(mobile, password, new Callback(){ onSuccess(response){ token = resonse.getToken() getUesrInfo(token, new Callback(){ onSuccess(response){ name = response.getName() display(name) }
onError(){ // } } }
onError(){ // }}
以上代码仅演示了2个接口串联调用的场景,我们需要2个callback对象,每个callback对象又包含成功和失败2个方法,想象一下,如果有更多的接口需要串联,则代码的逻辑分支就变成了复杂的树形结构,可读性很差;
在安卓开发中,容易陷入回调地狱的典型场景包括:
弹窗;
页面路由;
接口请求;
授权;
基于callback的三方库调用;
kotlin
Kotlin在2017年被谷歌宣布为Android的官方语言,Kotlin是一种由JetBrains开发的静态类型编程语言,它运行在Java虚拟机(JVM)上,也可以编译成JavaScript或本机代码。Kotlin的设计目标是成为一种现代化的、安全的、简洁的编程语言,能够在各种平台上进行开发,并且与Java互操作性良好。
Kotlin提供了协程的支持,这是一种轻量级的并发编程工具,可简化异步操作的管理。协程可以避免回调地狱,通过使用挂起函数(suspending functions)来简化异步代码的编写,使其看起来更像是同步代码,从而提高了代码的可读性和可维护性。
协程(Coroutine)
协程是一段代码,不同的协程之间可协作式的执行,协程和线程不是同一层次的东西,协程是建立在线程之上的概念,多个协程可跑在同一个线程,而一个协程也可以在多个线程之间切换。创建线程的代价是比较高的,通常你只能创建有限数量的线程,而协程是非常轻量级的,你几乎可创建任意多的协程。线程是由操作系统管理的,而协程是由kotlin库管理的。
为了用好协程,有几个基本的概念需要了解:
Builder
协程的构造器,用于新建一个协程,launch()和async()是两个最常见的构造函数,如果你不想从协程得到返回值,就用launch,否则用async;
Dispatcher
线程分发器,用于指定协程跑在哪个线程,一种典型的使用场景就是我们需要在IO线程做网络请求,然后回到UI线程操作View;常用的Dispatcher有Default,IO和Main,Default适用于在工作线程执行CPU密集型任务,IO适用于网络请求,Main适用于操作UI;
Scope
协程的上下文,用来管理协程的,每个协程都需要关联一个scope,常见的scope有Global Scope,LifeCycle Scope和ViewModel Scope。当你希望你的协程生命周期等同于整个app,就用Global Scope,当你希望协程的生命周期等同于Activity/Fragment的,则使用LifeCycle Scope,当你希望协程的生命周期等同于ViewModel的,就用ViewModel Scope。当scope的生命周期结束时,关联的协程也会被cancel。
Job
协程的句柄,当你调用launch或async的时候就会得到一个job,你可以调用Job的cancel()方法结束协程;
用协程消灭回调地狱
下面,我们将用协程依次消灭上述case中的回调地狱;
弹窗
lifecycleScope.launch { // 显示弹窗,并异步等待用户操作 val result = showDialog() toast("user clicked $result")}
suspend fun showDialog(): String { // 包装成suspend函数 return suspendCoroutine<String> { cont -> // 原有的基于callback的代码 SimpleDialog(this) .setContent("Choose yes or no") .setBtnCancel("no") { cont.resume("no") } .setBtnOk("yes") { cont.resume("yes") } .show() }}
我们用suspendCoroutine()函数将原来的基于回调的代码包装起来,这个函数提供一个Continuation,当callback发生的时候,可调用Continuation的resume(),于是调用方就能以同步的形式拿到返回值,并执行后续的逻辑;顺便提一句,你也可以调用Continuation.resumeWithException()方法抛出异常,调用方可用try-catch捕捉异常,用于处理某些异常场景;值得一提的是,suspend函数必须在另一个supend函数或者协程中调用,这个例子中,我们借助launch()函数创建了一个协程;
路由
class MyActivity : AppCompatActivity() { var requestCode = 1 var defer :CompletableDeferred<Intent?>? = null
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if(resultCode == requestCode){ defer.complete(data) } }
fun launchIntent(intent: Intent) : Deferred<ActivityResult?> { defer = CompletableDeferred<Intent?>() startActivityForResult(intent, requestCode) return defer }
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) button.setOnClickListener { lifecycleScope.launch(Dispatchers.Main) { val intent = Intent(...) // 发起路由,并异步等待返回值 val result = launchIntent(intent).await() result?.let { // 读取返回值,继续 } } } }}
调用startActivityForResult()的时候,我们新建一个CompletableDeferred对象,在onActivityResult()中,我们调用其complete()方法,并传入返回值;
调用方拿到CompletableDeferred实例后,调用await()异步等待返回值,拿到结果后继续后续流程;
实际使用的时候,可以将这段逻辑封装到基类,并可以维护一个requestCode到CompletableDeferred的Map,这样子类就无需重复编写这些代码了;
另外请注意,上面的代码未处理Activity在后台被杀并重启的场景;
接口请求
假设我们用retrofit库做网络请求,
public interface TaskService { @GET("/tasks") Call<User> getUser();}
以上代码声明了一个同步使用的接口,这个接口的返回值是Call,Call提供一个execute()方法;
fun getUser(): User{ TaskService taskService = ServiceGenerator.createService(TaskService.class); Call<User> call = taskService.getUser(); User user = call.execute().body(); return user}
定义一个方法,用于接口请求,并返回结果,注意,如果这个方法在UI线程直接调用,将阻塞UI线程,导致ANR;
lifecycleScope.launch { withContext(Dispatchers.IO) { val user = getUser() withContext(Dispatchers.Main) { showUser(user) } }}
Activity/Fragment提供lifcecycleScope,这个Scope是和组件的生命周期绑定的,当组件销毁的时候,相关的协程也会销毁,不用担心内存泄漏;我们调用launch()方法并传入一个lambda,在lambda内部,我们用withContext(Dispatchers.IO)将协程dispatch到IO线程,防止阻塞UI线程,等接口返回后,我们再次调用withContext(Dispatchers.Main)将协程dispatch到UI线程,将User信息显示在UI上;
在实际的开发中,还有一个很常见的场景,就是并发调用2个接口,等到2个接口全部返回结果后,显示数据。
lifecycleScope.launch { withContext(Dispatchers.IO) { val first: Deferred<User> = async { return getUser() }
val second: Deferred<Product> = async { return getProduct() } val user = first.await() val product = second.await()
withContext(Dispatchers.Main) { show(user, product) } }}
launch是start-and-forget模式,而async是start-and-get-result模式,允许从内部返回一个结果,我们将2个请求分别包在async中,他们会并行执行,然后我们调用await()等待返回值,等到他们全部返回结果后,后续的代码才会执行;
其他
授权和调用三方库的场景,类似弹窗的场景,不再赘述;
总结
作为原生的安卓开发,我们苦异步编程久已,自从kotlin和协程被引入了原生安卓开发,我们终于找到了优雅的异步编程写法,再也不用眼馋Javascript的async/await了。
作者介绍
Pony,现任移动研发资深专家
- 点赞
- 收藏
- 关注作者
评论(0)