[Android] Activity Result API を使う
startActivityForResult()
で別の Activity を呼び出して、 onActivityResult()
で結果を受け取るコードを書いていて、そういえば新しいAPIが出たんだったな、と思い出したので、使ってみました。
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()
}
取得した uri
を Toast
で表示するだけのコードです。
選択をキャンセルすると uri
は null
になります。わかりやすいですね。
呼び出し
Activityを起動するためのコードも書いていきます。
こちらのコードも公式ドキュメントでは getContent("image/*")
と関数のように書かれていますが、こちらも 1.2.0-alpha05
で invoke()
が削除され、明示的に 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
で結果を処理してもメリットは薄いです。
きちんと対応するには GetContent
や StartActivityForResult
の親クラスである 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 はとても良いものですね!
一応、このコードを試せるようにリポジトリへのリンクを貼っておきます。
Discussion