はじめに
Androidエンジニアの篠本(ささもと)です。
今回は、deprecated(非推奨)警告の解消のためにstartActivityForResultとonActivityResultをActivity Result APIに置き換える対応を行い、置き換え前後でコードの可読性が向上し、不具合が発生しにくくなるようにコード改善を実現できたので、その紹介をさせていただきます。
なお、この記事は以下の環境を前提としています。
- Kotlin 1.9.24
- Android APIレベル 34
startActivityForResultとonActivityResultとは?
ActivityのstartActivityForResultとonActivityResultとは、画面から別の画面へ遷移する際、遷移元の画面が遷移先の画面の結果を受け取れるようにするために使用されます。
具体的には、遷移元から遷移先の画面を呼び出す際にstartActivityForResultを使用し、遷移先の画面を識別するためのリクエストコードを指定します。そして、遷移元が遷移先の画面から結果を受け取る際にはonActivityResultをオーバーライドし、さきほどのリクエストコードを使用して結果を受け取ります。
具体的には以下のような実装になります。
// 遷移元の画面 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = DataBindingUtil.setContentView(this, R.layout.activity_main) binding.goToNextButton.setOnClickListener { // リクエストコードを指定して画面遷移する。 val intent = Intent(this, NextActivity::class.java) startActivityForResult(intent, REQUEST_CODE_NEXT) } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) // リクエストコードを使用して、遷移先の画面から結果を受け取る。 if (requestCode == REQUEST_CODE_NEXT && resultCode == RESULT_OK) { val result = data?.getStringExtra(NextActivity.KEY_RESULT).orEmpty() Log.d("MainActivity", "result: $result") } } private companion object { const val REQUEST_CODE_NEXT = 1 } }
// 遷移先の画面 class NextActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = DataBindingUtil.setContentView(this, R.layout.activity_next) // OKボタンが押された場合、結果を設定して画面を終了する。 binding.okButton.setOnClickListener { val intent = Intent().putExtra(KEY_RESULT, "ResultParam") setResult(RESULT_OK, intent) finish() } } companion object { const val KEY_RESULT = "KEY_RESULT" } }
startActivityForResultとonActivityResultについて整理すると、以下のようになります。
- startActivityForResult:遷移先画面を識別するリクエストコードを指定して画面遷移するメソッド
- onActivityResult:遷移先画面から結果を受け取るメソッド
deprecatedの内容と置き換え方法
これら従来のstartActivityForResultとonActivityResultについて、androidx.activityライブラリのv1.2.0からdeprecatedになりました。そして、新たにActivity Result APIの使用が推奨されるようになりました。
Activity Result APIにて画面遷移を行う場合、以下のように実装します。
- 遷移先の画面に対して、ActivityResultContractの継承クラスを実装する
- 遷移元の画面にて、ActivityResultLauncherを生成する
- 画面遷移するときは、launch()を呼ぶ
まず、ActivityResultContractの継承クラスでは、createIntent()とparseResult()をオーバーライドします。
createIntent()では遷移先の画面に遷移する際に使用するIntentの生成を実装し、parseResult()では遷移元の画面へ返す結果を実装します。
具体的には、以下のような実装になります。
class NextActivityResultContract : ActivityResultContract<Unit, String>() { override fun createIntent(context: Context, input: Unit): Intent { return Intent(context, NextActivity::class.java) } override fun parseResult(resultCode: Int, intent: Intent?): String { return when (resultCode) { Activity.RESULT_OK -> { intent?.getStringExtra(KEY_RESULT).orEmpty() } else -> "" } } }
遷移先の画面の呼び出しとその結果の受け取りについては、以下のように実装します。
まず、さきほど実装したNextActivityResultContractを使用してActivityResultLauncherを生成します。そして、画面遷移するときにはlaunch()を呼びます。
class MainActivity : AppCompatActivity() { private val launcher = registerForActivityResult(NextActivityResultContract()) { result -> // ここで遷移先からの結果を受け取る。 Log.d("MainActivity", "result: $result") } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = DataBindingUtil.setContentView(this, R.layout.activity_main) binding.goToNextButton.setOnClickListener { // 画面遷移する。 launcher.launch() } } }
なお、遷移先の画面(今回の例ではNextActivity)については、実装を変える必要はありません。
置き換えのメリット
ここまでActivity Result APIへの置き換えについて説明しましたが、ここではActivity Result APIを使用するメリットについて説明します。Activity Result APIを使用するメリットについては、主に以下の2つが上げられます。
- 遷移元画面の結果受け取り処理の可読性向上
- ActivityResultContractによる返り値の型の強制
以下、具体的に説明します。
遷移元画面の結果受け取り処理の可読性向上
さきほど説明した例では遷移先の画面が1つだけでしたが、実際には遷移先の画面が複数になる場合があります。その場合、onActivityResultにおいて遷移先の画面毎に条件分岐を実装する必要があります。onActivityResultという1つのメソッドの中にすべての画面の結果受け取り処理が集中することになり、コードの可読性を低下させます。
具体的には以下のようになります。
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // 省略 } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) when (requestCode) { REQUEST_CODE_FIRST -> { // First画面の結果を受け取る。 } REQUEST_CODE_SECOND -> { // Second画面の結果を受け取る。 } REQUEST_CODE_THIRD -> { // Third画面の結果を受け取る。 } // 以下、画面毎に分岐が続く… } } }
これをActivity Result APIに置き換えた例が以下になります。1つのメソッドにすべての画面の結果受け取り処理が集中するのではなく、画面毎にブロックが分かれているので、従来のonActivityResultより可読性が向上します。
class MainActivity : AppCompatActivity() { private val firstLauncher = registerForActivityResult(FirstActivityResultContract()) { result -> // First画面の結果を受け取る。 Log.d("MainActivity", "First result: $result") } private val secondLauncher = registerForActivityResult(SecondActivityResultContract()) { result -> // Second画面の結果を受け取る。 Log.d("MainActivity", "Second result: $result") } private val thirdLauncher = registerForActivityResult(ThirdActivityResultContract()) { result -> // Third画面の結果を受け取る。 Log.d("MainActivity", "Third result: $result") } // 以下、画面毎にlauncherの宣言が続く… override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // 省略 } }
ActivityResultContractによる返り値の型の強制
従来のonActivityResultにて結果を受け取る場合、受け取る側で型を指定して結果を受け取る必要があります。このため、遷移先画面で指定する型と遷移元画面の結果受け取りで指定する型が異なる場合、遷移元画面にて正しく結果を受け取れない場合があります。
具体的には以下のような場合です。
// 遷移元の画面 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = DataBindingUtil.setContentView(this, R.layout.activity_main) binding.goToNextButton.setOnClickListener { val intent = Intent(this, NextActivity::class.java) startActivityForResult(intent, REQUEST_CODE_NEXT) } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == REQUEST_CODE_NEXT && resultCode == RESULT_OK) { // 結果の型が異なるため、正しく結果を受け取ることが出来ない。 val result = data?.getIntExtra(NextActivity.KEY_RESULT, /* defaultValue: */ 0) Log.d("MainActivity", "result: $result") } } private companion object { const val REQUEST_CODE_NEXT = 1 } }
// 遷移先の画面 class NextActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = DataBindingUtil.setContentView(this, R.layout.activity_next) binding.okButton.setOnClickListener { // 文字列型で結果を返す。 val intent = Intent().putExtra(KEY_RESULT, "ResultParam") setResult(RESULT_OK, intent) finish() } } companion object { const val KEY_RESULT = "KEY_RESULT" } }
Activity Result APIにて結果を受け取る場合、ActivityResultContractの継承クラスにて型を定義するだけでよいため、結果を返す側と結果を受け取る側で型の不一致が起こることはないです。
class NextActivityResultContract : ActivityResultContract<Unit, String>() { override fun createIntent(context: Context, input: Unit): Intent { return Intent(context, NextActivity::class.java) } override fun parseResult(resultCode: Int, intent: Intent?): String { return when (resultCode) { Activity.RESULT_OK -> { intent?.getStringExtra(KEY_RESULT).orEmpty() } else -> "" } } }
class MainActivity : AppCompatActivity() { // 結果resultの型はNextActivityResultContractにて定義されているため、遷移元画面で指定する必要がない。 private val launcher = registerForActivityResult(NextActivityResultContract()) { result -> Log.d("MainActivity", "result: $result") } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // 省略 } }
最後に
今回、startActivityForResultとonActivityResultの警告解消のため、画面遷移をActivity Result APIに置き換えることで、コードの可読性が向上し不具合が発生しにくいコードを実現できることを新たに学び、ローソンアプリのコード改善を実現できました。
今後もローソンデジタルイノベーションでは技術ブログを更新していきますので、是非「読者になる」で応援していただけますと幸いです。