Android 超详细深刨Activity Result API的使用

如果你将项目中的appcompat库升级到1.3.0或更高的版本,你会发现startActivityForResult()方法已经被废弃了。

这个方法相信所有做过Android的开发者都用过,它主要是用于在两个Activity之间交换数据的。

那么为什么这个如此常用的方法会被废弃呢?官方给出的说法是,现在更加建议使用Activity Result API来实现在两个Activity之间交换数据的功能。

我个人的观点是,startActivityForResult()方法并没有什么致命的问题,只是Activity Result API在易用性和接口统一性方面都做得更好。既然有更好的API,那么就不再建议去使用过去老旧的API,所以才把startActivityForResult()方法标为了废弃。

其实除了startActivityForResult()方法之外,还有像requestPermissions()方法也被标为了废弃。看起来它们两者之间好像并没有什么关联,但是到了Activity Result API中,它们就被归属到了统一的API模板当中。因此,我们可以使用非常类似的代码去实现在两个Activity之间交换数据,以及请求运行时权限的功能。

另外,Activity Result API的用法非常简单,一学就会。相信你看完本篇文章之后,就可以将自己项目中所有相关的代码都升级成Activity Result API的用法。

那么我们开始吧。

在两个Activity之间交换数据

如果想要在两个Activity之间交换数据,我们先回顾一下传统的写法:

class FirstActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_first)
        val firstButton = findViewById<Button>(R.id.first_button)
        firstButton.setOnClickListener {
            val intent = Intent(this, SecondActivity::class.java)
            startActivityForResult(intent, 1)
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        when (requestCode) {
            1 -> {
                if (resultCode == RESULT_OK) {
                    val data = data?.getStringExtra("data")
                    // Handle data from SecondActivity
                }
            }
        }
    }

}

这里调用了startActivityForResult()方法去向SecondActivity请求数据,然后在onActivityResult()方法中去解析SecondActivity返回的结果。

那么SecondActivity中的代码是什么样的呢?这里我们就简单模拟一下,随便返回一个数据即可:

class SecondActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_second)
        val secondButton = findViewById<Button>(R.id.second_button)
        secondButton.setOnClickListener {
            val intent = Intent()
            intent.putExtra("data", "data from SecondActivity")
            setResult(RESULT_OK, intent)
            finish()
        }
    }

}

如此一来,FirstActivity向SecondActivity请求数据的功能就通了,是不是感觉也挺简单的?所以我刚才说了,startActivityForResult()方法并没有什么致命的问题。

那么接下来我们学习一下如何使用Activity Result API来实现同样的功能。

首先,SecondActivity中的代码是不需要修改的。这部分代码并没有被废弃,Activity Result API也与它无关。

FirstActivity中的代码,我们需要使用Activity Result API来替代startActivityForResult()的写法,如下所示:

class FirstActivity : AppCompatActivity() {

    private val requestDataLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
        if (result.resultCode == RESULT_OK) {
            val data = result.data?.getStringExtra("data")
            // Handle data from SecondActivity
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_first)
        val firstButton = findViewById<Button>(R.id.first_button)
        firstButton.setOnClickListener {
            val intent = Intent(this, SecondActivity::class.java)
            requestDataLauncher.launch(intent)
        }
    }

}

注意这里的代码变更。我们完全移除了对onActivityResult()方法的重写,而是调用registerForActivityResult()方法来注册一个对Activity结果的监听。

registerForActivityResult()方法接收两个参数,第一个参数是一种Contract类型,由于我们是希望从另外一个Activity中请求数据,因此这里使用了StartActivityForResult这种Contract。第二个参数是一个Lambda表达式,当有结果返回时则会回调到这里,然后我们在这里获取并处理数据即可。

registerForActivityResult()方法的返回值是一个ActivityResultLauncher对象,这个对象当中有一个launch()方法可以用于去启用Intent。这样我们就不需要再调用startActivityForResult()方法了,而是直接调用launch()方法,并把Intent传入即可。

这两种写法到底孰优孰劣呢?我个人感觉还是Activity Result API的写法更简单一点,不过总体优势并没有那么大。Activity Result API真正的优势在于我们接下来要讲的内容。

请求运行时权限

除了startActivityForResult()方法之外,requestPermissions()方法也被废弃了。至于理由都是一样的,推荐使用Activity Result API。

那么要如何使用Activity Result API来请求运行时权限呢?不要惊讶,它将会出奇得简单:

class FirstActivity : AppCompatActivity() {

    private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
        if (granted) {
            // User allow the permission.
        } else {
            // User deny the permission.
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_first)
        val firstButton = findViewById<Button>(R.id.first_button)
        firstButton.setOnClickListener {
            requestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
        }
    }

}

我们只需关注代码变更的部分。

由于这次是请求运行时权限,因此不能再使用刚才的StartActivityForResult来作为Contract了,而是要使用RequestPermission这种Contract。

另外由于指定了不同的Contract类似,Lambda表达式的参数也会发生变化。现在Lambda表达式会传入一个布尔型的参数,用于告诉我们用户是否允许了我们请求的权限。

最后,launch()方法的参数也发生了变化,现在只需传入要请求的权限名即可。

有没有发现,这两段代码的模板出奇得一致。我们使用了两段差不多的代码,实现了之前几乎并没有太大联系的两个功能。这就是Activity Result API的好处,它将一些API的接口统一化,使得我们在实现特定功能的时候能够变得非常简单。

内置Contract

刚才我们体验了StartActivityForResult和RequestPermission这两种Contract,分别用于在两个Activity之间交换数据,以及请求运行时权限。它们都是Activity Result API中内置的Contract。

那么除此之外,我们还有哪些内置Contract可以使用呢?

下面是我列出的appcompat 1.3.0版本所支持的所有内置Contract,以后还可能会继续增加新的Contract:

StartActivityForResult()
StartIntentSenderForResult()
RequestMultiplePermissions()
RequestPermission()
TakePicturePreview()
TakePicture()
TakeVideo()
PickContact()
GetContent()
GetMultipleContents()
OpenDocument()
OpenMultipleDocuments()
OpenDocumentTree()
CreateDocument()

每个Contract的命名已经明确表示它们的作用是什么了,也就是说,当我们要实现以上Contract所包含的功能时,都不需要再自己手动费力去写了,Activity Result API已经帮我们支持好了。

比如,我想要调用手机摄像头去拍摄一张图片,并且得到这张图片的Bitmap对象,那么就可以使用TakePicturePreview这个Contract。

实现代码如下:

class FirstActivity : AppCompatActivity() {

    private val takePictureLauncher = registerForActivityResult(ActivityResultContracts.TakePicturePreview()) { bitmap ->
        // bitmap from camera
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_first)
        val firstButton = findViewById<Button>(R.id.first_button)
        firstButton.setOnClickListener {
            takePictureLauncher.launch(null)
        }
    }

}

代码非常简单,就是换了一下Contract类型,然后Lambda表达式的参数会变成bitmap对象。另外由于TakePicturePreview这个Contract不需要输入参数,所以我们调用launch()方法的时候直接传入null就可以了。

看到这里,可能有些读者朋友会比较好奇。我怎么知道每种Contract要求什么输入参数,以及Lambda表达式中返回的参数是什么呢?

这个很简单,只需要看一下这个Contract的源码即可。比如TakePicturePreview的源码如下:

/**
 * An {@link ActivityResultContract} to
 * {@link MediaStore#ACTION_IMAGE_CAPTURE take small a picture} preview, returning it as a
 * {@link Bitmap}.
 * <p>
 * This can be extended to override {@link #createIntent} if you wish to pass additional
 * extras to the Intent created by {@code super.createIntent()}.
 */
public static class TakePicturePreview extends ActivityResultContract<Void, Bitmap> {
    ...
}

我们暂时不用关心TakePicturePreview内部的具体实现,只要看一下它在继承父类时指定的泛型类型即可。其中第一个参数就是要求的输入参数,而第二个参数就是Lambda表达式返回的输出参数。

只要掌握这个小技巧,每种Contract你就都能轻松运用自如了。那么我就不再多做演示,剩下这些Contract的用法等待你自己去探索。

自定义Contract

除了以上内置Contract之外,我们确实也可以定义自己的Contract类型。

虽然我觉得这个必要性并不是很强,因为内置Contract已经可以帮助我们应对绝大多数场景了。

不过,自定义Contract并不是一件复杂的事情。相反,它非常简单,所以这里还是简略提一下吧。

刚才我们大概看到了TakePicturePreview的源码实现,它必须继承自ActivityResultContract类,并通过泛型来指定当前Conract类型的输入参数和输出参数。

ActivityResultContract是一个抽象类,它的内部定义了两个抽象方法,如下所示:

public abstract class ActivityResultContract<I, O> {

    public abstract @NonNull Intent createIntent(@NonNull Context context, I input);

    public abstract O parseResult(int resultCode, @Nullable Intent intent);
    ...
}

也就是说,任何一个继承自ActivityResultContract的Contract,都需要重写createIntent()和parseResult()这两个方法。

而这两个方法的作用也非常明显。createIntent()就是用于创建一个Intent,后续会使用这个Intent来发起动作,比如启动另外一个Activity去获取数据,或者打开相机去拍照等等。而parseResult()则是用于解析响应的结果,并把解析出来的结果作为输出参数返回到Lambda表达式当中。

每一个内置的Contract都是使用的这种规则来封装的自己的逻辑。

那么我们要自定义一个什么样的Contract来进行演示呢?

我想了一下,刚才在编写两个Activity之间交换数据的时候,我们需要显示地启动SecondActivity,并手动将SecondActivity返回的数据从Intent中解析出来,这就稍微有些麻烦。而借助自定义Contract就可以对此处进行优化。

新建一个叫做GetDataFromSecondActivity的Contract,代码如下所示:

class GetDataFromSecondActivity : ActivityResultContract<Void, String?>() {

    override fun createIntent(context: Context, input: Void?): Intent {
        return Intent(context, SecondActivity::class.java)
    }

    override fun parseResult(resultCode: Int, intent: Intent?): String? {
        if (resultCode == Activity.RESULT_OK) {
            if (intent != null) {
                return intent.getStringExtra("data")
            }
        }
        return null
    }

}

我们通过泛型指定,这个Contract的输入参数是Void,输出参数是一个字符串。

然后在createIntent()方法中,我们手动创建了一个Intent,并将它的用途设置为打开SecondActivity。

最后在parseResult()方法中,我们对SecondActivity返回的结果进行解析,并将解析出来的字符串作为输出参数返回。

这样一个自定义的Contract就完成了,而我们使用这个Contract再去实现最开始的在两个Activity之间交换数据的功能,就会变得更加简单:

class FirstActivity : AppCompatActivity() {

    private val getDataLauncher = registerForActivityResult(GetDataFromSecondActivity()) { data ->
        // Handle data from SecondActivity
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_first)
        val firstButton = findViewById<Button>(R.id.first_button)
        firstButton.setOnClickListener {
            getDataLauncher.launch(null)
        }
    }

}

可以看到,借助GetDataFromSecondActivity这个Contract,我们不需要再显式地声明去启动SecondActivity,launch()方法直接传入null即可。另外,我们也不需要再去手动解析SecondActivity返回的数据,lambda表达式上的参数就是解析出来的结果了。

最后一个小问题

到这里,我们基本就将Activity Result API的所有内容都学完了。

在本篇文章的最后,我想再回答一个小问题。因为我自己当初在使用Activity Result API的时候产生过这样的疑惑,所以我猜或许也会有朋友有同样的问题,那么在这里就顺手解答了。

现在你已经知道,Activity Result API是可以完全取代startActivityForResult()方法的。但是我们在调用startActivityForResult()方法时,除了传入Intent之外,还需要再传入一个requestCode,用于在多个任务之间进行区分。比如如下代码:

class FirstActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_first)
        val firstButton = findViewById<Button>(R.id.first_button)
        val secondButton = findViewById<Button>(R.id.second_button)
        firstButton.setOnClickListener {
            val intent = Intent(Intent.ACTION_VIEW)
            startActivityForResult(intent, 1)
        }
        secondButton.setOnClickListener {
            val intent = Intent(Intent.ACTION_DIAL)
            startActivityForResult(intent, 2)
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        when (requestCode) {
            1 -> {
                // Handle result for ACTION_VIEW
            }
            2 -> {
                // Handle result for ACTION_DIAL
            }
        }
    }

}

这里我们分别在两处调用了startActivityForResult()方法,它们各自用于处理不同的任务,因此需要给它们设置不同的requestCode。

在onActivityResult()方法当中,我们为了区分这个结果是来自之前的哪个任务的,所以要在这里再对requestCode进行判断。

这是以前使用startActivityForResult()方法时的传统写法。

而Activity Result API是没有地方让你传入requestCode的。

我在刚接触Activity Result API的时候受思维惯性的影响被这个问题困扰了一下,没有地方传入requestCode该怎么办呢?

后来思维转过来弯之后发现,原来Activity Result API根本就不需要requestCode这种东西,我们可以使用如下写法来实现和刚才完全一样的功能:

class FirstActivity : AppCompatActivity() {

    private val actionViewLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
        // Handle result for ACTION_VIEW
    }

    private val actionDialLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
        // Handle result for ACTION_DIAL
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_first)
        val firstButton = findViewById<Button>(R.id.first_button)
        val secondButton = findViewById<Button>(R.id.second_button)
        firstButton.setOnClickListener {
            val intent = Intent(Intent.ACTION_VIEW)
            actionViewLauncher.launch(intent)
        }
        secondButton.setOnClickListener {
            val intent = Intent(Intent.ACTION_DIAL)
            actionDialLauncher.launch(intent)
        }
    }

}

由此也可以看出,Activity Result API的设计更加合理,不需要借助requestCode这种魔术数字也能对多个任务进行区分。

一切都更加简单和清晰。

到此这篇关于Android 超详细深刨Activity Result API的使用的文章就介绍到这了,更多相关Android Activity Result 内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Android中两个类让你再也不用实现onActivityResult()

    前言 当我们知道fragment仅仅可以收到自己请求的回调后,我们就可以用它来解耦activity中onActivityResult的逻辑了.一般情况下,如果一个activity中多个回调结果的话,onActivityResult()中会有各种case判断,并且这种设计也打断了流式写法.那么我们索性建立一个无UI的fragment做精准的接收器,这样就可以再也不写onActivityResult()了. Fragment: /** * @author Kale * @date 2018/4/13

  • Android基础之startActivityForResult()的用法详解

    前言 安卓开发中一个很基础的操作就是打开一个 Activity ,另一个很必要的操作就是,打开一个 Activity ,在打开的 Activity 中操作之后并获得返回结果. 两个 Activity 为了演示这个操作,我们需要两个 Activity : MainActivity 和 OtherActivity ,在 MainActivity 中通过 Intent 启动 OtherActivity ,并获得 OtherActivity 结束后返回的结果,例子仅作演示,为了方便,我直接在 Java

  • Android startActivityForResult和setResult的区别

     Android startActivityForResult和setResult的区别 startActivityForResult与startActivity的不同之处在于: 1.startActivity( ) 仅仅是跳转到目标页面,若是想跳回当前页面,则必须再使用一次startActivity( ). 2.startActivityForResult( ) 可以一次性完成这项任务,当程序执行到这段代码的时候,假若从T1Activity跳转到下一个Text2Activity,而当这个Tex

  • Android startActivityForResult实例详解

    Android startActivityForResult实例详解 startActivityForResult用于两个activity之间的数据传递,Activity1传值给Activity2,Activity2再返回值给Activity1. 第一步:Activity1中:startActivityForResult(Intent intent, Int requestCode) Intent intent = new Intent(); intent.setClass(Activity1.

  • Android onActivityResult和setResult方法详解及使用

    Android onActivityResult和setResult方法 最近做公司项目遇到Android onActivityResult和setResult ,在应用过程中进程报错,这里有必要记录下,以免再次使用出错. 如果你想在Activity中得到新打开Activity关闭后返回的数据,你需要使用系统提供的startActivityForResult(Intent intent,int requestCode)方法打开新的Activity,新的Activity关闭后会向前面的Activi

  • Android 超详细深刨Activity Result API的使用

    如果你将项目中的appcompat库升级到1.3.0或更高的版本,你会发现startActivityForResult()方法已经被废弃了. 这个方法相信所有做过Android的开发者都用过,它主要是用于在两个Activity之间交换数据的. 那么为什么这个如此常用的方法会被废弃呢?官方给出的说法是,现在更加建议使用Activity Result API来实现在两个Activity之间交换数据的功能. 我个人的观点是,startActivityForResult()方法并没有什么致命的问题,只是

  • Android 超详细SplashScreen入门教程

    这次的Android系统变化当中,UI的变化无疑是巨大的.Google在Android 12中采取了一种叫作Material You的界面设计,一切以你为中心,以你的喜好为风格.相信大家一旦上手Android 12之后应该能立刻察觉到这些视觉方面的变化. 关于这个SplashScreen,今天就值得好好讲一讲了. 什么是SplashScreen SplashScreen其实通俗点讲就是指的闪屏界面.这个我们国内开发者一定不会陌生,因为绝大多数的国内App都会有闪屏界面这个功能,很多的App还会利

  • Android 超详细讲解fitsSystemWindows属性的使用

    对于android:fitsSystemWindows这个属性你是否感觉又熟悉又陌生呢? 熟悉是因为大概知道它可以用来实现沉浸式状态栏的效果,陌生是因为对它好像又不够了解,这个属性经常时灵时不灵的. 其实对于android:fitsSystemWindows属性我也是一知半解,包括我在写<第一行代码>的时候对这部分知识的讲解也算不上精准.但是由于当时的理解对于我来说已经够用了,所以也就没再花时间继续深入研究. 而最近因为工作的原因,我又碰上了android:fitsSystemWindows这

  • Android超详细介绍自定义多选框与点击按钮跳转界面的实现

    总程:在avtivity_main.xml设计5个控件,btn1-5,点击btn1弹出一个多选对话框,点击按钮btn1弹出一个多选框可选择你喜欢的打野英雄,点击btn2跳转到activity_main2界面(就是图片,不可选择)设计思路流程:在activity_main.xml布局界面,总体在头目录进行垂直排列,然后镶嵌5个水平的线性布局(左是ImageView,右边是Button按钮)由于5张图的大小在一个屏幕显示不出来,所以添加一个ScoveView滚动,以使所有资源可以看到! 在MainA

  • Android超详细讲解组件AdapterView的使用

    目录 概述 介绍AdapterView的编程模式 Adapter ListView使用 myAdapater.java MainActivity.java activity_main.xml myAdapater.java MainActivity.java 概述 在Android应用开发中,AdapterView是一类常用且非常重要的组件.我们常见的以列表的形式显示信息的组件就是AdapterView的子类,称为Listview:我们经常以网格方式浏览图片缩略图的组件也是AdapterView

  • Android超详细讲解弹出多选框的实现

    目录 程序代码功能:点击一个按钮弹出一个多选框 在activity_main.xml布局一个button控件,大小,颜色,位置,背景可自行调节,以被用来在MainActivity.java调用其id来实现点击弹出多选框!在btn1.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) {}}大括号内放入点击btn1时间要发生的内容,因为是弹出多选框,所以用来Alter.Bu

  • Android超详细讲解组件LinearLayout的使用

    目录 概述 常用XML配置属性 (1) android:orientation (2) android:gravity (3) View中继承来的属性 代码举例 概述 LinearLayout是线性布局组件,放置在其中的组件按列或者按行(就是垂直或者水平)的方式排序分布. 常用XML配置属性 (1) android:orientation 设置LinearLayout容器布局组件的方式:只能取值:horizontal(水平的),vertical(垂直的) (2) android:gravity

  • Android超详细讲解组件ScrollView的使用

    目录 概述 练习 HorizontalScrollView: 概述 ScrollView也是一个容器,它是FrameLayout的子类,它的主要作用就是将超出物理屏幕的内容显示出来,(就是滚动条效果)ScrollView提供垂直滚动,进而可将超出物理屏幕的内容显示出来. 在一般情况下,可以将一个采用垂直方式布局组件的LinearLayout作为ScrollLayout容器的子组件,同时,在LinearLayout容器中可以显示超出屏幕物理高度的内容. 练习 这么说有点抽象,然后我们现在实现完成一

  • Android Activity的生命周期与加载模式超详细图文解析

    目录 Activity的生命周期 Activity的加载模式 Activity的生命周期 Activity是安卓应用的重要组成单元之一,其对于安卓的作用类似于Servlet对于Web应用的作用. 整个Activity生命周期的图解如下 具体的生命周期可以总结成如下几个步骤 onCreate():Activity启动后第一个被调用的函数,常用来进行Activity的初始化,如创建View,绑定数据和恢复数据. onStart():当Activity显示在屏幕上时,函数被调用. onRestart(

  • Android全面屏适配与判断超详细讲解

    目录 1.全面屏的适配 2.判断是否为全面屏 3.全面屏手机的虚拟导航和全面屏手势的判断 1.全面屏的适配 全面屏出现后,如果不做适配,屏幕上会出现上下黑边,影响视觉效果. 针对此问题,Android官方提供了适配方案,即提高App所支持的最大屏幕纵横比,实现起来也比较简单,在AndroidManifest.xml中做如下配置即可,在AndroidManifet里的下声明: <meta-data android:name="android.max_aspect" android:

随机推荐