用Kotlin协程消灭安卓开发中的回调地狱

原创 Pony 拍码场

安卓原生开发的痛点

Android平台推出以来,Java一直是开发Android应用的主要语言。尽管后来Kotlin成为了谷歌主推的编程语言,Java仍然被广泛使用,在Android开发中仍占有重要地位。从github的数据看,用Java写的安卓项目仍然是Kotlin2倍以上;我们的项目创建较早,大部分代码是用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个接口串联调用的场景,我们需要2callback对象,每个callback对象又包含成功和失败2个方法,想象一下,如果有更多的接口需要串联,则代码的逻辑分支就变成了复杂的树形结构,可读性很差;

在安卓开发中,容易陷入回调地狱的典型场景包括:

弹窗;

页面路由;

接口请求;

授权;

基于callback的三方库调用;

kotlin

Kotlin2017年被谷歌宣布为Android的官方语言,Kotlin是一种由JetBrains开发的静态类型编程语言,它运行在Java虚拟机(JVM)上,也可以编译成JavaScript或本机代码。Kotlin的设计目标是成为一种现代化的、安全的、简洁的编程语言,能够在各种平台上进行开发,并且与Java互操作性良好。

Kotlin提供了协程的支持,这是一种轻量级的并发编程工具,可简化异步操作的管理。协程可以避免回调地狱,通过使用挂起函数(suspending functions)来简化异步代码的编写,使其看起来更像是同步代码,从而提高了代码的可读性和可维护性。

协程(Coroutine)

协程是一段代码,不同的协程之间可协作式的执行,协程和线程不是同一层次的东西,协程是建立在线程之上的概念,多个协程可跑在同一个线程,而一个协程也可以在多个线程之间切换。创建线程的代价是比较高的,通常你只能创建有限数量的线程,而协程是非常轻量级的,你几乎可创建任意多的协程。线程是由操作系统管理的,而协程是由kotlin库管理的。

为了用好协程,有几个基本的概念需要了解:

Builder

协程的构造器,用于新建一个协程,launch()async()是两个最常见的构造函数,如果你不想从协程得到返回值,就用launch,否则用async

Dispatcher

线程分发器,用于指定协程跑在哪个线程,一种典型的使用场景就是我们需要在IO线程做网络请求,然后回到UI线程操作View;常用的DispatcherDefault,IOMainDefault适用于在工作线程执行CPU密集型任务,IO适用于网络请求,Main适用于操作UI

Scope

协程的上下文,用来管理协程的,每个协程都需要关联一个scope,常见的scopeGlobal Scope,LifeCycle ScopeViewModel Scope。当你希望你的协程生命周期等同于整个app,就用Global Scope,当你希望协程的生命周期等同于Activity/Fragment的,则使用LifeCycle Scope,当你希望协程的生命周期等同于ViewModel的,就用ViewModel Scope。当scope的生命周期结束时,关联的协程也会被cancel

Job

协程的句柄,当你调用launchasync的时候就会得到一个job,你可以调用Jobcancel()方法结束协程;

用协程消灭回调地狱

下面,我们将用协程依次消灭上述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发生的时候,可调用Continuationresume(),于是调用方就能以同步的形式拿到返回值,并执行后续的逻辑;顺便提一句,你也可以调用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()异步等待返回值,拿到结果后继续后续流程;

实际使用的时候,可以将这段逻辑封装到基类,并可以维护一个requestCodeCompletableDeferredMap,这样子类就无需重复编写这些代码了;

另外请注意,上面的代码未处理Activity在后台被杀并重启的场景;

接口请求

假设我们用retrofit库做网络请求,

public interface TaskService {      @GET("/tasks")    Call<User> getUser();}

以上代码声明了一个同步使用的接口,这个接口的返回值是CallCall提供一个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)将协程dispatchIO线程,防止阻塞UI线程,等接口返回后,我们再次调用withContext(Dispatchers.Main)将协程dispatchUI线程,将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)        }    }}

launchstart-and-forget模式,而asyncstart-and-get-result模式,允许从内部返回一个结果,我们将2个请求分别包在async中,他们会并行执行,然后我们调用await()等待返回值,等到他们全部返回结果后,后续的代码才会执行;

其他

授权和调用三方库的场景,类似弹窗的场景,不再赘述;

总结

作为原生的安卓开发,我们苦异步编程久已,自从kotlin和协程被引入了原生安卓开发,我们终于找到了优雅的异步编程写法,再也不用眼馋Javascriptasync/await了。

作者介绍

Pony,现任移动研发资深专家

  • 7
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值