ActivityResultContractを使ってonActivityResultに別れを告げよう


## はじめに
皆さんは`onActivityResult`はお好きですか？
`requestCode`と`resultCode`の字面が似ていて間違えやすかったり、`if`や`when`によってコードの見通しが悪くなりやすいので個人的にはあまり好きではありません。
そんな悩みを解決してくれる`ActivityResultContract`が最近発表されたktxのアップデート（2020/04/12時点ではα版）に来たようなので纏めておきます。

## 検証
検証用に2画面だけの雑なアプリを作りました。
![activities.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68671/5e217272-5c20-232e-be58-44ec617dc89a.png)

### Before
`MainActivity`はボタンを押すと`startActivityForResult`で`SecondActivity`を起動し、`SecondActivity`が終了されると`onActivityResult`で結果を受け取ります。

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {

    companion object {
        private const val REQUEST_CODE = 1234
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        button_open.setOnClickListener {
            startActivityForResult(
                    SecondActivity.createIntent(this),
                    REQUEST_CODE
            )
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        Log.d("MainActivity", "requestCode: $requestCode, resultCode: $resultCode, data: $data")
    }
}
```

`SecondActivity`はボタンを押すと`RESULT_OK`をセットして自身を終了します。
バックキー押下で閉じた場合には`RESULT_CANCELED`扱いになります。

```kotlin:SecondActivity.kt
class SecondActivity : AppCompatActivity() {

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

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_second)

        button_close.setOnClickListener {
            setResult(Activity.RESULT_OK, Intent().putExtra("data", "hoge"))
            finish()
        }
    }
}

```

ログ出力はこんな感じになりました。

```shell
# バックキーで閉じた
2020-04-13 03:22:27.578 28835-28835/com.masaibar.activityresultcontractsample D/MainActivity: requestCode: 1234, resultCode: 0, data: null

# ボタンで閉じた
2020-04-13 03:22:34.206 28835-28835/com.masaibar.activityresultcontractsample D/MainActivity: requestCode: 1234, resultCode: -1, data: Intent { (has extras) }
```

### After
ここから`MainActivity`を書き換えていきます。
`SecondActivity`には変更はありません。

`app/build.gradle`にライブラリを追加します。
（今回はActivityでの検証なのでFragmentの方はコメントアウトしています。）

```gradle:app/build.gradle
    implementation "androidx.activity:activity-ktx:1.2.0-alpha03"
    // implementation "androidx.fragment:fragment-ktx:1.3.0-alpha03"
```

`MainActivity`はこの様に書き換えることが出来ました。
`ActivityResultContract`自体は抽象クラスで、ここでは継承した[StartActivityForResult](https://developer.android.com/reference/androidx/activity/result/contract/ActivityResultContracts.StartActivityForResult)を使っています。
`onActivityResult`だけでなく`REQUEST_CODE`も不要になり、かなりスッキリした記述になります。

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {

    private val launcher: ActivityResultLauncher<Intent> =
            prepareCall(ActivityResultContracts.StartActivityForResult()) { activityResult ->
                Log.d("MainActivity", activityResult.toString())
            }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        button_open.setOnClickListener {
            launcher.launch(SecondActivity.createIntent(this))
        }
    }
}
```

ログ出力は下記のようになりました。

```shell
# バックキーで閉じた
2020-04-13 03:24:01.444 29039-29039/com.masaibar.activityresultcontractsample D/MainActivity: ActivityResult{resultCode=RESULT_CANCELED, data=null}

# ボタンで閉じた
2020-04-13 03:24:31.886 29039-29039/com.masaibar.activityresultcontractsample D/MainActivity: ActivityResult{resultCode=RESULT_OK, data=Intent { (has extras) }}
```

コード差分も置いておきます。
https://github.com/masaibar/ActivityResultContractSample/pull/1/files
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68671/de46b8ed-b034-1146-b71f-eb85b09ef58c.png)

##その他のユースケースについて
先程も触れましたが`ActivityResultContract`は抽象クラスであり、継承したいくつかのクラスが用意されています。
参考：https://developer.android.com/reference/androidx/activity/result/contract/ActivityResultContract

例えば**ファイル一覧から画像を取得する場合**ならば従来は下記のようなコードが必要でしたが

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {
    companion object {
        private const val REQUEST_CODE_CHOOSER = 1234
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        button_get_content.setOnClickListener {
            startActivityForResult(
                    Intent(Intent.ACTION_GET_CONTENT).apply {
                        addCategory(Intent.CATEGORY_OPENABLE)
                        type = "image/*"
                    },
                    REQUEST_CODE_CHOOSER
            )
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == REQUEST_CODE_CHOOSER && resultCode == Activity.RESULT_OK) {
            Log.d("MainActivity", "uri: ${data?.data}")
        }
    }
}

```

`ActivityResultContracts.GetContent`を用いると下記のようにシンプルに書くことができるようになります。

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {

    private val launcher = prepareCall(ActivityResultContracts.GetContent()) { uri ->
        Log.d("MainActivity", "uri: $uri")
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        button_get_content.setOnClickListener {
            launcher.launch("image/*")
        }
    }
}
```

現在ライブラリ側に定義されているのは下記の13種類です。
ぱっと見た感じファイル取得以外だと撮影系や、パーミッションに関連するものがありますね。
もしお目当てのクラスがここに無かった場合でも自身で[ActivityResultContractsの継承クラスを実装することが可能](https://developer.android.com/training/basics/intents/result#custom)です。

```
ActivityResultContracts.CreateDocument
ActivityResultContracts.GetContent
ActivityResultContracts.GetContents
ActivityResultContracts.OpenDocument
ActivityResultContracts.OpenDocumentTree
ActivityResultContracts.OpenDocuments
ActivityResultContracts.PickContact
ActivityResultContracts.RequestPermission
ActivityResultContracts.RequestPermissions
ActivityResultContracts.StartActivityForResult
ActivityResultContracts.TakePicture
ActivityResultContracts.TakePicturePreview
ActivityResultContracts.TakeVideo
```

## おわりに
`ActivityResultContracts`の原理などはtakahiromさんの[ActivityResultContractのコードを少し読んでみる](https://qiita.com/takahirom/items/658ebabea3dc8c1c5a6c)が詳しいです。

まだα版ということで心理的に導入しづらいかも知れませんが、コードの見通しがかなりスッキリするため`onActivityResult`と別れたい方は検討する価値があるのではないでしょうか。
願わくば早く正式版としてアップデートが来ますように🙏

## 参考
https://developer.android.com/training/basics/intents/result
https://qiita.com/takahirom/items/658ebabea3dc8c1c5a6c