Activityの起動処理は起動されるActivityで実装すべし


Androidアプリの開発を始めたら、かなり初期に習得するであろう他のActivityを起動する方法ですが、いわゆる入門書とかに書かれているような実装方法はよろしくない。って話をしようと思います。
最近はNavigation Component使っているしSingle Activityだから使う機会が無い?知らんがな。

MainActivityからMainActivity2を起動する例を考えます。

入門書の延長

意外とよく見かけるのが以下のように呼び出し元でIntentをつくってstartActivityをコールする方法。入門書に書いてある感じです。ExrtaのKeyをConstantsに定数定義しているのもあるあるですね。

MainActivity.kt
findViewById<View>(R.id.button).setOnClickListener {
    startActivity(Intent(context, MainActivity2::class.java).also {
        it.putExtra(Constants.EXTRA_HOGEHOGE, hogehoge)
    })
}

受け取った側はこんな風になりますね。

MainActivity2
class MainActivity2 : AppCompatActivity() {
    private var hogehoge: String? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main2)
        hogehoge = intent.getStringExtra(Constants.EXTRA_HOGEHOGE)
    }
}

この書き方はサンプルコード以外には使用しない方がよいですね。
Extraの対応が呼び出し元と呼び出し先に別れてしまっているため、適切に使うには、呼び出し元が、呼び出し先の仕様を知っている必要があり、インターフェースによる制約を受けることができません。

Activityの起動処理は起動されるActivityで実装すべし

やはり起動処理は起動されるActivityの中に閉じさせた方が良いですよね。
なのでクラスメソッドとして実装するのが定石だと思います。

MainActivity2
class MainActivity2 : AppCompatActivity() {
    private var hogehoge: String? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main2)
        hogehoge = intent.getStringExtra(EXTRA_HOGEHOGE)
    }

    companion object {
        private const val EXTRA_HOGEHOGE = "EXTRA_HOGEHOGE"

        fun start(context: Context, hogehoge: String) {
            context.startActivity(
                Intent(context, MainActivity2::class.java).also {
                    it.putExtra(EXTRA_HOGEHOGE, hogehoge)
                }
            )
        }
    }
}

こうすれば、呼び出しはメソッド呼び出しになりますので、呼び出しのインターフェースが明確になります。
また、これら引数をどのようにIntentに格納するのか、どのように読み出されるのかを利用側が知る必要がなくなりますね。

MainActivity.kt
findViewById<View>(R.id.button).setOnClickListener {
    MainActivity2.start(this, "hogehoge")
}

ExtraのKeyもAction名も定義はそれをするクラスに閉じた形で定義することができ、クラス外部ではその値がどうなっているかを知る必要がありません。Extraへの格納と読み出しが一つのクラスの中で閉じるため、型安全ではないままではありますが対応のチェックなどはやりやすくなります。

startActivity自体は呼び出し元のActivityなりに紐付く処理ですし、PendingIntentが欲しい場合なども考えると、Intentの作成処理だけを呼び出し先のActivityに持たせた方が良いかもしれません。

引数が増えてきた場合は引数クラスに分離する

起動に使用するパラメータが増えてくるとExtraを個別に扱っていると対応が分かりにくくなり、型安全に扱いたくなります。
引数をIntentに格納、読み出しをするためのArgumentsクラスをつくって、それを経由させるのが良いでしょう。
Extraに押し込める段階で型情報は失われますが、対応はこのデータクラスに閉じているため外部からは型安全に使えます。

MainActivity2Arguments.kt
data class MainActivity2Arguments(
    val hogehoge: String
) {
    fun putToIntent(intent: Intent) {
        intent.putExtra(EXTRA_HOGEHOGE, hogehoge)
    }

    companion object {
        private const val EXTRA_HOGEHOGE = "EXTRA_HOGEHOGE"

        fun fromIntent(intent: Intent): MainActivity2Arguments =
            MainActivity2Arguments(
                hogehoge = intent.getStringExtra(EXTRA_HOGEHOGE) ?: ""
            )
    }
}

Intentへの格納、読み出しがこのデータクラスに閉じており、利用しているActivityもそれを知る必要がなくなります。またEXTRAの定義もActivityが知る必要がなくなりますね。
当然Intentに他のExtraを設定しない前提ではありますが。

MainActivity2
class MainActivity2 : AppCompatActivity() {
    private lateinit var arguments: MainActivity2Arguments

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main2)
        arguments = MainActivity2Arguments.fromIntent(intent)
    }

    companion object {
        fun start(context: Context, hogehoge: String) {
            val arguments = MainActivity2Arguments(hogehoge)
            context.startActivity(
                Intent(context, MainActivity2::class.java).also {
                    arguments.putToIntent(it)
                }
            )
        }
    }
}

ArgumentsクラスはSerializableをそのままExtraに入れてしまえばいいじゃないという意見もあるかもしれませんが、Serializableを適切に使うにはいろいろ配慮が必要で、その辺をよく理解せずに使ってしまい、問題を起こす事例を何度も見たことがあるため個人的にはSerializalbleは使いたくない派です。(あくまで個人的意見)

まとめ

startActivityを呼び出す、少なくともIntentを作るのは、そのIntentを読み出す、呼び出される側のActivityに閉じたところで行うべきだ、というお話でした。

以上です。