AndroidのstartActivityForResultに変わる新しいAPIが追加されました

2020.05.19

まえがき

Activityで他のActivityに処理を依頼して、その結果を処理するために、startActivityForResult()/onActivityResult()があります。現時点では、まだalphaですがActivity1.2.0、Fragment1.3.0から新しいAPIが追加されました。

ActivityResultRegistry: ComponentActivity で ActivityResultRegistry が利用できるようになり、アクティビティまたはフラグメントのメソッドをオーバーライドせずに startActivityForResult() + onActivityResult()、requestPermissions() + onRequestPermissionsResult() の各フローを処理できるようになりました。ActivityResultContract によりタイプ セーフティーが向上し、これらのフローをテストするためのフックを提供するようになりました。更新された アクティビティから結果を取得するをご覧ください。(b/125158199)

注: Activity 1.2.0-alpha02 へのアップグレードを行う場合、RequestPermission または RequestPermissions の Activity Result コントラクトを使用するには、Fragment 1.3.0-alpha02 へのアップグレードが必要です。

やってみる

日本語はまだないので英語でみるとドキュメントが新しいやり方に変わっています。

Getting a result from an activity

今回やったサンプルは以下の通りです。

  • Activityから暗黙/明示的intentで結果をもらうケース
  • Fragmentから暗黙/明示的intentで結果をもらうケース

サンプルはこちらです。

kamedon/AndroidIntentResult

また、今回はアルファ機能を使っていますので、リリース時には異なる可能性があります。

def activity_version = "1.2.0-alpha04"
implementation "androidx.activity:activity-ktx:$activity_version"

def fragment_version = "1.3.0-alpha04"
implementation "androidx.fragment:fragment-ktx:$fragment_version"
debugImplementation "androidx.fragment:fragment-testing:$fragment_version"

Activityから暗黙/明示的intentで結果をもらうケース

暗黙Intent

以下は、暗黙Intentでギャラリーアプリなどから、写真をもらうケースです。かなりシンプルになっています。registerForActivityResultで送受信を作っています。 呼ぶ側で、従来でいうところのContentTypeを指定して読んでるだけです。第2引数にOptionを設定できますが今回は割愛します。

class SelectPhotoActivity : AppCompatActivity(R.layout.activity_select_photo) {

    val getContent = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
        Log.d("result", "activity: $uri")
    }


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        selectBtn.setOnClickListener {
            getContent("image/*")
        }
    }

}

明示的Intent

以下は、ResultActivityからResultを取得するサンプルになります。

明示的のときはStartActivityForResultを使用することで、従来のようなActivityResultが返ってくるようになるので、あとは同じように処理できます。startForResultを呼ぶとき、どのActivityを呼ぶか指定します。

class CustomActivity : AppCompatActivity(R.layout.activity_custom) {
    val startForResult =
        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
            if (result.resultCode == Activity.RESULT_OK) {
                val intent = result.data
                Toast.makeText(
                    this,
                    "result: ${intent?.getStringExtra("result")}",
                    Toast.LENGTH_SHORT
                ).show()
            }
        }


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        startBtn.setOnClickListener {
            startForResult(Intent(this, ResultActivity::class.java))
        }
    }
}

Fragmentから暗黙/明示的intentで結果をもらうケース

ケースはActivityと同じですので割愛します。

今回は試していないですが、同様なことができるはずです。しかしLifecycleを通じて送受信する方法が推奨されています。Lifecycle経由にすることでDestroy処理もやってくれることがポイントです。Lifecycle経由しない場合は、登録/開放処理をいれるようにしましょう。

When using the ActivityResultRegistry APIs, it's strongly recommended to use the APIs that take a LifecycleOwner, as the LifecycleOwner automatically removes your registered launcher when the Lifecycle is destroyed. However, in cases where a LifecycleOwner is not available, each ActivityResultLauncher class allows you to manually call unregister() as an alternative.

Getting a result from an activity

以下のサンプルは、追加でLifecycleのライブラリーを追加しています。

def lifecycle_version = "2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"

暗黙Intent

基本的にはActivityと同じですが、LifecycleObserverの生成時にGetContentを生成し、Lifecycleに登録しています。

class SelectPhotoFragment : Fragment(R.layout.fragment_select_photo) {
    lateinit var observer: MyLifecycleObserver

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        observer = MyLifecycleObserver(requireActivity().activityResultRegistry)
        lifecycle.addObserver(observer)
        selectBtn.setOnClickListener {
            observer.selectImage()
        }
    }
}

class MyLifecycleObserver(private val registry: ActivityResultRegistry) : DefaultLifecycleObserver {
    lateinit var getContent: ActivityResultLauncher<String>

    override fun onCreate(owner: LifecycleOwner) {
        getContent =
            registry.register(
                "key",
                owner,
                ActivityResultContracts.GetContent(),
                ActivityResultCallback {
                    Log.d("result", "fragment: $it")
                })
    }

    fun selectImage() {
        getContent("image/*")
    }
}

明示的Intent

GetContentのかわりStartActivityForResultを使用し、Lifecycleに登録すればOKです

class CustomFragment : Fragment(R.layout.fragment_custom) {

    private lateinit var observer: CustomLifecycleObserver

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        observer = CustomLifecycleObserver(requireActivity().activityResultRegistry)
        lifecycle.addObserver(observer)
        startBtn.setOnClickListener {
            observer.start(requireContext())
        }
    }
}

class CustomLifecycleObserver(private val registry: ActivityResultRegistry) :
    DefaultLifecycleObserver {

    private lateinit var startForResult: ActivityResultLauncher<Intent>

    override fun onCreate(owner: LifecycleOwner) {
        startForResult =
            registry.register("key", owner, ActivityResultContracts.StartActivityForResult(),
                ActivityResultCallback {
                    Log.d("result", "fragment: ${it.data?.getStringExtra("result")}")
                })
    }

    fun start(context: Context) {
        startForResult(Intent(context, ResultActivity::class.java))
    }
}

まとめ

所感かなり簡単で安全になったと思います。リクエストコードの指定などの間違いがなくなるのではないでしょうか。テストもしやすくなったみたいなのでまた次回に。