用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,现任移动研发资深专家

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/19436.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

洗地机什么品牌质量好?家用洗地机排行榜

一年一度的大促节又到了&#xff0c;各大电商平台和实体店纷纷推出力度不小的折扣活动&#xff0c;吸引着消费者的关注和购买欲望。很多家庭也趁着这个机会&#xff0c;购置一些智能家居产品来提升生活品质。其中&#xff0c;洗地机作为近年来发展迅速的明星产品&#xff0c;受…

Redis篇 哈希表在redis中的命令

哈希命令 一.哈希表的基本认识二. 哈希表在redis中的命令1.hset,hget2.hdel3.hkeys,hvals4.hexists5.hgetall6.hmget7.hlen8.hincrby和hincrbyfloat 一.哈希表的基本认识 在JAVA数据结构中&#xff0c;我们就已经接触到了哈希表&#xff0c; 在当时&#xff0c;我们主要用到的哈…

人工智能和大模型的区别

人工智能&#xff08;AI&#xff09;和大模型是两个相关但有区别的概念。理解它们之间的区别有助于更好地掌握现代科技的发展动态。 人工智能&#xff08;AI&#xff09; 人工智能&#xff08;Artificial Intelligence, AI&#xff09;是一个广义的概念&#xff0c;指的是通过…

React18 apexcharts数据可视化之折线图

基础折线图 import ApexChart from react-apexcharts;export function Basic() {// 数据序列const series [{name: "Desktops",data: [10, 41, 35, 51, 49, 62, 69, 91, 148]},]// 图表选项const options {// 图表chart: {height: 650,type: line,zoom: {enabled:…

springboot发送短信验证码,结合redis 实现限制,验证码有效期2分钟,有效期内禁止再次发送,一天内发送超3次限制

springboot结合redis发送短信验证码,实现限制发送操作 前言(可忽略)实现思路正题效果图示例手机号不符合规则校验图成功发送验证码示例图redis中缓存随机数字验证码&#xff0c;2分钟后失效删除redis缓存图验证码有效期内 返回禁止重复发送图验证码24小时内发送达到3次&#xf…

【Leetcode 206】 反转链表——此递归相当妙啊

题目 给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5] 输出&#xff1a;[5,4,3,2,1]示例 2&#xff1a; 输入&#xff1a;head [1,2] 输出&#xff1a;[2,1]示例 3&#xff1a; …

【机器学习300问】105、计算机视觉(CV)领域有哪些子任务?

计算机视觉作为人工智能的重要分支&#xff0c;发展至今已经在诸多领域取得显著的成果。在众多的计算机视觉任务中&#xff0c;图像分类、目标检测与定位、语义分割和实例分割是四个基本而关键的子任务&#xff0c;它们在不同的应用场景下扮演着重要角色。这四个子任务虽然各具…

深入理解JVM:内存结构、垃圾收集与性能调优

目录 JDK、JRE、JVM关系? 启动程序如何查看加载了哪些类&#xff0c;以及加载顺序? class字节码文件10个主要组成部分? JVM结构 画一下JVM内存结构图 程序计数器 Java虚拟机栈 本地方法栈 Java堆 方法区 运行时常量池? 什么时候抛出StackOverflowError? 例如&…

海医大三院使用先进血管外科微创技术成功救治危重主动脉夹层患者

近日,上海东方肝胆外科医院血管外科周建教授团队采用主动脉弓分支型一体化移植物联合体外开窗技术,成功救治复杂危重主动脉夹层患者,为上海嘉定首例,彰显了上海东方肝胆外科医院血管外科的优势与特色。 患者谢先生,72岁,两周前突发剧烈胸背部撕裂样疼痛,休息后症状未能得到缓解…

多模态开源项目实战(https://github.com/QwenLM/Qwen-VL)

GitHub - HqWu-HITCS/Awesome-Chinese-LLM: 整理开源的中文大语言模型&#xff0c;以规模较小、可私有化部署、训练成本较低的模型为主&#xff0c;包括底座模型&#xff0c;垂直领域微调及应用&#xff0c;数据集与教程等。 1.AttributeError: ChatGLMTokenizer object has n…

量化交易:如何在QMT中运行Python策略并在VSCode中高效调试?

哈喽&#xff0c;大家好&#xff0c;我是木头左&#xff01; 为何选择QMT和VSCode进行量化策略开发&#xff1f; 在量化交易的世界里&#xff0c;选择正确的工具与拥有优秀的策略同等重要。调用用Visual Studio Code&#xff08;简称VSCode&#xff09;或pycharm&#xff0c;方…

JAVA 大鱼吃小鱼小游戏

java实现大鱼吃小鱼&#xff0c;支持身份证防沉迷、账号密码、选择难度 放沉迷 登录 选择难度 游戏界面

【移除链表元素】python

目录 题目&#xff1a; 方法&#xff1a; 知识&#xff1a; 代码&#xff1a; 题目&#xff1a; 方法&#xff1a; 在头节点前增加一个虚拟头节点 知识&#xff1a; 链表中的每一个节点只包含当前值val和指向下一个next 代码&#xff1a; class Solution:def removeEle…

uniapp或微信小程序一些问题解决

1.按钮边框如何去除&#xff1f; 参考博主&#xff1a;微信小程序按钮去不掉边框_微信小程序button去掉边框-CSDN博客文章浏览阅读1k次。最近在学uni-app&#xff0c;顺便自己写个小程序。左上角放了个button&#xff0c;可边框怎么也去不掉…原来微信小程序的按钮要去掉边框要…

汽车IVI中控开发入门及进阶(二十一):DAB和FM 收音机

前言: 在过去的十年里,数字收音机对车载娱乐产生了重大影响。现在,几乎每辆新车都标配了这项技术,这也是我们60%以上的人收听收音机的方式。甚至有传言称,在不久的将来,将永久关闭调频发射机,使许多车载收音机过时。但一些相对年轻的汽车在工厂里仍然没有安装DAB,而且…

jdk1.8和jdk18的区别

JDK 1.8&#xff08;也称为Java 8&#xff09;和JDK 18是Java开发工具包&#xff08;Java Development Kit&#xff09;的两个不同版本。虽然它们都是JDK&#xff0c;但由于发布时间相差多年&#xff0c;它们在功能、性能、语言特性和工具支持等方面有显著差异。以下是它们的主…

ipv4的掩码长度到掩码地址转换

ipv4的地址用32位二进制数表示&#xff0c;为了显示方便&#xff0c;一般是按4段十进制数表示&#xff0c;每段取值范围是0-255&#xff0c;对应二进制数00000000-11111111 有些场合&#xff0c;掩码是用4段十进制数表示&#xff0c;跟ip地址一样&#xff0c;另外一些场合是用0…

模型蒸馏(distillation)

大size的teacher模型&#xff0c;训练的样本&#xff0c;最后一层softmax之前的logits&#xff0c;当作student模型的训练目标&#xff0c;损失函数是2个向量的距离&#xff1b; 原理&#xff1a;logits包含更多的信息&#xff0c;比label(也就是1-hot vector)的信息量更大&am…

六西格玛培训:企业逆袭的秘密武器!——张驰咨询

为了提升企业的运营效率、产品质量和客户满意度&#xff0c;六西格玛培训成为了一个不可或缺的环节。以下是企业成功实施六西格玛培训的关键步骤&#xff1a; 一、清晰设定培训目标 首先&#xff0c;企业应明确六西格玛培训的具体目标&#xff0c;如提升产品质量、降低成本、…

java Web开发中采用Servlet登录验证,中文用户名始终提示“用户名密码错误”以及输出中文乱码问题

采用Servlet登录验证&#xff0c;中文乱码问题解决 在Java Web开发中&#xff0c;往往采用Servlet完成前后端直接的控制和处理&#xff0c;例如&#xff1a;用户登录验证功能。 在采用如下Servle源码t完成用户名登录验证时&#xff0c;只要用户名涉及中文&#xff0c;对于正确…