Android-ActivityResultAPI

发布时间 2023-11-29 17:48:36作者: 夜行过客

Android-ActivityResultAPI

1. 推出目的

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

image-20231129173545784

现在更加建议使用Activity Result API来实现在两个Activity之间交换数据的功能。

2. 传统写法 - 在两个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()方法并没有什么致命的问题。

3. 新写法 - 在两个Activity之间交换数据

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传入即可。

4. 使用 Activity Result API 进行权限处理

ex: 申请读SDcard的权限:

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()方法的参数也发生了变化,现在只需传入要请求的权限名即可。

5. 其它内置 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的用法等待你自己去探索。

6. 为什么不需要 requestCode?

现在你已经知道,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这种魔术数字也能对多个任务进行区分。

参考链接

Activity Result API详解,是时候放弃startActivityForResult了

<完>