[Android] Activity Result API を使う

12 min読了の目安(約7400字TECH技術記事

startActivityForResult() で別の Activity を呼び出して、 onActivityResult() で結果を受け取るコードを書いていて、そういえば新しいAPIが出たんだったな、と思い出したので、使ってみました。

startActivityForResult、 onActivityResult は Activity 1.2.0-alpha04 で Deprecated にされています。

gradle の設定

Activity Result API は、上記ドキュメントによると Activity 1.2.0-alpha02 で導入されたようです。
現時点(2020/09/24)での最新バージョンは 1.2.0-alpha08 なので、 build.gradle(:app) に次のように記述します。

dependencies {
    :
    implementation 'androidx.activity:activity-ktx:1.2.0-alpha08'
    implementation 'androidx.fragment:fragment-ktx:1.3.0-alpha08'
    :
}

最初は activity の方だけ書いたのですが、 fragment の方も書かないと例外が発生するようです。

暗黙的 Intent の処理

まずはドキュメントにサンプルとして書かれている GetContent で動作を試してみます。

結果の処理

公式ドキュメントには prepareCall() というメソッドが書かれていますが、これは 1.2.0-alpha04 で名前が registerForActivityResult() に変わったようです。

    private val getContent = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
        Toast.makeText(this, uri?.toString() ?: "cancelled", Toast.LENGTH_SHORT).show()
    }

取得した uriToast で表示するだけのコードです。
選択をキャンセルすると uri は null になります。わかりやすいですね。

呼び出し

Activityを起動するためのコードも書いていきます。
こちらのコードも公式ドキュメントでは getContent("image/*") と関数のように書かれていますが、こちらも 1.2.0-alpha05invoke() が削除され、明示的に launch() を呼ぶよう変更が入ったようです。

        selectButton.setOnClickListener {
            getContent.launch("image/*")
        }

この数行のコードでファイルを選択する Activity を起動し、選択されたファイルへの Uri を取得する処理が書けてしまいました。すごい!シンプルですね!

元々は…

元々は次のようなコードを書く必要がありました。 ACTION_GET_CONTENT を指定して暗黙的な Intent を投げるコードです。この変化はだいぶ嬉しいですね。

const val REQUEST_IMAGE_GET = 1

fun selectImage() {
    val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
        type = "image/*"
    }
    if (intent.resolveActivity(packageManager) != null) {
        startActivityForResult(intent, REQUEST_IMAGE_GET)
    }
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
    if (requestCode == REQUEST_IMAGE_GET && resultCode == Activity.RESULT_OK) {
        val thumbnail: Bitmap = data.getParcelableExtra("data")
        val fullPhotoUri: Uri = data.data
        // Do work with photo saved at fullPhotoUri
        ...
    }
}

独自 Activity の処理

用意されたアクション以外でも Activity の結果を処理する必要があります。
そんなときは GetContent の代わりに、StartActivityForResult を利用することで、汎用的な Intent を処理することができます。

呼び出す Activity

次のような Activity を呼び出します。
起動したら、 "Result" という名前で結果を詰めて返すだけの Activity です。

class StringActivity : AppCompatActivity() {

    companion object {
        fun createIntent(context: Context): Intent {
            return Intent(context, StringActivity::class.java)
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setResult(RESULT_OK, Intent().apply {
            putExtra("Result", "success")
        })
    }
}

結果の処理

StartActivityForResult を使って処理します。
Request Code を使った分岐がないものの、これまでの startActivityForResult と onActivityResult を使ったコードとあまり差がないように感じます。

    private val getActivityResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
        if (result.resultCode == RESULT_OK) {
            val string = result.data?.getStringExtra("Result") ?: "empty"
            Toast.makeText(this, string, Toast.LENGTH_SHORT).show()
        } else {
            Toast.makeText(this, "Error", Toast.LENGTH_SHORT).show()
        }
    }

呼び出し

呼び出しは launch() を呼び出すだけです。
GetContent では "image/*" という文字列を渡していましたが、こちらは Intent を渡します。

            val intent = StringActivity.createIntent(this)
            getActivityResult.launch(intent)

これで一般的な Intent を処理することができます。

メリットはあるのか

自分で作った Activity を StartActivityForResult を使って処理してみましたが、どうもメリットが薄いような気がします。
launch() では Intent を渡せるので、間違った Intent を渡してしまうということも有り得そうです。
また、結果を処理するのも呼び出し側で、どんなデータが詰められたのかを呼び出し側が把握していないといけません。
なんとなくですが、将来的に onActivityResult が使えなくなったときに、機械的にコードを置き換えられるようにするためのコードという印象です。

新 API にきちんと対応するには

StartActivityForResult で結果を処理してもメリットは薄いです。
きちんと対応するには GetContentStartActivityForResult の親クラスである ActivityResultContract を継承したクラスを作るのが良いでしょう。

ActivityResultContract を継承する

先ほど作った StringActivity の中に ActivityResultContract を継承した GetString というクラスを作ります。

ActivityResultContract  |  Android デベロッパー  |  Android Developers

ActivityResultContract は I と O の2つの型引数を取ります。I は Input で、 O は Output ですね。
StringActivity は Input がなかったので I は Unit、 Output は文字列を返したいので String を指定します。

次の2つのメソッドを実装します。

  • createIntent(context: Context, input: I): Intent
  • parseResult(resultCode: Int, intent: Intent?): O

前者は Activity を起動するための Intent を返すメソッドです。
後者は onActivityResult() で行っていたような処理をして、本当に必要なデータを返すためのメソッドです。

    class GetString : ActivityResultContract<Unit, String?>() {
        override fun createIntent(context: Context, input: Unit?): Intent {
            return createIntent(context)
        }

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

onActivityResult() で処理していたコードを StringActivity 内に持ってくることができました。
呼び出し側は「どのようなデータ」が「何というキー名」で格納されているのかを把握する必要がなくなりました。

結果の処理

GetString クラスを作ったので、呼び出し側では次のようなコードで結果を処理することができるようになります。

    private val getString = registerForActivityResult(StringActivity.GetString()) { result: String? ->
        Toast.makeText(this, result ?: "Error", Toast.LENGTH_SHORT).show()
    }

呼び出し

StringActivity の起動もシンプルになります。すばらしいですね!

            getString.launch(Unit)

雑感

StartActivityForResult でコードを書いていたときは、この新APIは「どこがいいんだろうか?」と疑問でした。
しかし、きちんとそれぞれの Activity で ActivityResultContract を継承したクラスを用意するようにすると、これまでどうしても Activity の外側に漏れ出ていたものが、きちんとその Activity の中に閉じ込めることができて、呼び出し側コードがすごく気持ちよく書けるようになりました。
新しい Activity Result API はとても良いものですね!

一応、このコードを試せるようにリポジトリへのリンクを貼っておきます。
https://github.com/t2low/ActivityResultApiSample

参考情報