Activity新的回调方式

在我们的日常开发中,从一个Activity打开另一个Activity并接收其回调结果是一个很普遍的场景,这其中包括打开其他应用的Activity,通常我们的做法都是通过activity.startActivityForResult()方法来实现。这其中除了我们应用内业务相关页面的跳转之外,还有很多像打开相册、调起相机等和系统Activity交互的场景,这些场景每次都要很麻烦的维护很多代码,即使是原生页面跳转也要维护requestCode和bundle数据的key值等常量,代码臃肿难以维护。Jetpack出现后,现在在处理这种非单向操作的场景时有了一种更优雅的方式,我们来看看它是怎么帮我们封装的。

首先,我们需要添加两个依赖(androidx.*中的)

api 'androidx.fragment:fragment:1.3.0-alpha06'
api 'androidx.activity:activity:1.2.0-alpha06'

然后我们顺着Activity的继承关系找下去会看到ComponentActivity的继承关系:

image

其中的ActivityResultCaller是注册回调的关键接口,ActivityResultRegistryOwner是用来获取Activity的ActivityResultRegistry对象的,通常用于Fragment或者其他非Activity类使用。

ActivityResultCaller接口中有两个方法:

@NonNull
<I, O> ActivityResultLauncher<I> registerForActivityResult(
        @NonNull ActivityResultContract<I, O> contract,
        @NonNull ActivityResultCallback<O> callback);
@NonNull
<I, O> ActivityResultLauncher<I> registerForActivityResult(
        @NonNull ActivityResultContract<I, O> contract,
        @NonNull ActivityResultRegistry registry,
        @NonNull ActivityResultCallback<O> callback);

这两个方法的差别就是三参数的那个多了个ActivityResultRegistry类型的参数,如果在Activity中调用则不需要这个参数,因为ComponentActivity中帮我们维护了一个ActivityResultRegistry对象了,如果在自定义组件或者其它类中可以使用三参数方法并调用activity的getActivityResultRegistry()方法传入该参数。

现在我们知道了每一个ComponentActivity的子类都可以使用这两个方法注册ActivityResult回调,我们从注册开始顺着看看是怎么实现的。

img

我们这里在Activity中注册,所以使用两个参数的注册方法。第一个参数就是Contract契约,是一个ActivityResultContract类型的对象,里面有两个关键方法,createIntent()和parseResult(),createIntent()就是来创建用于请求打开新Activity的intent的;其中的第二个参数是泛型,可以自定义,parseResult()用于最后根据resultCode和data创建ActivityResult对象传给ActivityResultCallback的onActivityResult()方法内使用,后面会看到。

第二个参数就是ActivityResultCallback对象了,重写onActivityResult方法就可以处理回调了,这就相当于之前我们在Activity中重写的onActivityResult方法。

image-20200703141056335

进到registerForActivityResult方法里面可以看到两个参数的registerForActivityResult也是调用了三参数的重载方法,ActivityResultRegistry对象用的是ComponentActivity内部定义的mActivityResultRegistry,三参数的方法最终返回的是mActivityResultRegistry的register方法的调用,这个方法有四个参数,后面两个就是ActivityResultContract对象和ActivityResultCallback对象,第一个参数是key值,稍后会提到,第二个参数是LifecycleOwner对象,这里就是Activity本身(因为ComponentActivity实现了它),现在我们进到ActivityResultRegistry类的register里面看看:

image-20200703141309938

我们会看到首先通过registerKey方法获取requestCode:

image-20200703141558444

这里可以看到requestCode是通过当前类里面的一个AtomicInteger对象产生的,所以每次产生的都会是最新且唯一的(这里注意getAndIncrement()方法返回的是增加前的值),这就不需要我们自己去维护requestCode了,然后把它保存在map里,这里有两个map,一个是mKeyToRc,它保存的是key->requestCode,另一个是mRcToKey,它保存的是requestCode-key,后面会看到他们的用武之地。

接着有一个mKeyToCallback的map以传进来的key值为键,保存了一个组合了callback和contract的CallbackAndContract对象,接下来根据lifecycleOwner获取到Activity的生命周期对象。

接下来的mPendingResults部分的代码主要是为了处理Activity非主动销毁时的恢复,后面会提到,值得注意的是,这里会使用正在进行注册的新callback来处理之前未正常执行完成的result。这里判断如果存在尚未正常执行完的回调且Activity此时生命周期已是STARTED之后(onStart()方法执行完),则先执行未完成的回调;若是生命周期在STARTED之前则给其生命周期添加监听,STARTED之后自动执行回调。

然后统一添加销毁时的注销工作的监听,最后会返回一个ActivityResultLauncher对象,我们就用这个对象来启动指定的Activity。

launcher.launch("image/*")

因为我们之前使用的contract是ActivityResultContracts.GetContent(),所以这里我们传入一个String作为intent的type。

image-20200703142447801

我们看到ActivityResultLauncher类里有两个launch方法,只有一个是需要重写的,因为ActivityOptionsCompat参数是可选的,ActivityOptionsCompat就是一个封装了Activity之间转换动画的类(还有一点其他设置),这里没传就是null。

我们上面说到这个launcher实际上就是那个register方法返回的匿名对象,所以这里的launch方法就是调用的那个匿名对象的launch方法,回过头去看,匿名对象的launch方法实际上是调用的ActivityResultRegistry类的onLaunch方法,并且把生成的requestCode、contract、传入的“iamge/*”、还有一个ActivityOptionsCompat对象带过去,所以到了这里实际上调用的就是ComponentActivity里的mActivityResultRegistry的onLaunch方法:

image-20200703143800799

onLaunch()方法是ActivityResultRegistry类里唯一的abstract方法。我们看看它都做了些什么:

首先通过contract的getSynchronousResult()方法拿到一个ActivityResultContract.SynchronousResult(这个类中只有一个泛型对象)对象,如果这个对象不为空则直接在主线程调用dispatchResult()方法结束launch,ActivityResultContract的getSynchronousResult()方法有默认实现(默认返回null),是一个可选方法,RequestPermission和RequestMultiplePermissions里会用得到,主要是用它处理一些不需要启动Activity就能知道预期结果的场景。dipatchResult方法很简单,不做说明了,看下图:

image-20200703150421901

接下来就是正常情况下最后启动Activity的流程了,可以看到出了两个特殊情况需要特别处理之外,其他的启动方式最后都是调用了我们熟悉的ActivityCompat.startActivityForResult(activity, intent, requestCode, optionsBundle)方法,ActivityResultContracts里面已经给我们封装好了常用的启动系统Activity的场景,如果我们需要启动自己的Activity,我们也可以使用ActivityResultContracts中的StartActivityForResult来实现。

到现在为止,我们可以很明显地感受到这种方式使我们启动Activity变得简洁优雅,但还有最后一个问题,最终的回调是如何触发的呢?前面我们看到了Immediate result path的回调,现在我们来看一下启动Activity之后返回的回调是怎么触发的。

官方文档中并没有回调触发的新方法,所以我们猜测应该还是finish()这个老朋友的任务:

image-20200703153317825

通过注释我们知道最终会回调到调用者的onActivityResult()方法:

image-20200703153556965

注释告诉我们已经不提倡在Activity里重写这个方法来接收回调了,提倡使用我们新介绍的这种方式。可以看到在这个统一的回调方法里,会首先调用mActivityResultRegistry的dispatchResult方法,如果返回false则继续走传统的回调方式:

image-20200703153928351

这里通过requestCode拿到一开始注册的key,再通过key拿到CallbackAndContract对象,最终执行doDispatch()方法:

image-20200703154224412

这里的resultCode和data都是上一个Activity调用setResult()方法设置的。可以看到,在这里调用了contract的parseResult方法拿到ActivityResult对象,回调到前面注册时callback的onActivityResult方法中,else分支的意义在于如果出现异常没取到回调对象(有可能是注册的时候传入的callback参数是null,虽然registerForActivityResult的callback参数加了注解明确表示不能为空,但是因为Java不像kotlin那样传入null会报错,所以这里还是要特殊容错处理)则会放到表示未处理的mPendingResults中,等到有新的注册行为时会通过新注册的callback对象来处理,前面已经提过。

至此,一个完整的新式启动回调流程就完成了。
不仅如此,这种方式内部还会调用onSaveInstance和onRestoreInstance方法进行key和回调等实例的保存和恢复,因此在那些回调尚未完成就由系统杀死的情况下,在返回重新创建实例后会自动回复之前的回调实例完成回调。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,185评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,652评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,524评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,339评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,387评论 6 391
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,287评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,130评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,985评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,420评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,617评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,779评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,477评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,088评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,716评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,857评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,876评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,700评论 2 354

推荐阅读更多精彩内容