みずきち日記

ひらすらプログラミング

Avtivity への遷移に SafeArgs を使ってみる

Android Jetpack のひとつに Navigation があります.

この機能を使えば複雑な Fragment 同士の画面遷移をGUI(xml)上で定義し,画面遷移をより簡潔に記述することが出来ます.

Fragment 間の画面遷移において,値渡しをする際は SafeArgs が便利です.

これは Navigation Graph の xml 内で対象 Fragment が必要とする引数を定義し,ビルド時に専用のクラスを生成し,それを使うことでより型安全に画面遷移を可能にする機能です.

今回は, 例のような Fragment --> Fragment だけでなく, Activity を立ち上げる際にも SafeArgs を使ってより型安全に画面遷移できる方法を考えてみます.

例として, 以下のような Activity があるとします.

class ResultActivity : AppCompatActivity() {

  companion object {
    private const val EXTRA_GITHUB_ID = "EXTRA_GITHUB_ID"

    fun createIntent(
      context: Context,
      githubId: String
    ): Intent {
      val intent = Intent(context, ResultActivity::class.java)
      intent.putExtra(EXTRA_GITHUB_ID, githubId)
      return intent
    }
    ...
}

よくある Activity を立ち上げるために必要な Intent を作成する方法として, 対象 Activity クラス内に static な createIntent メソッドを定義し, そこで Context やその他引数を受け取って, Intent に詰め込んで返す方法が一般的だと思います.

そして,呼び出し元では受け取った Intent を startActivity に渡します.

startActivity(ResultActivity.createIntent(this, "mzkii"))

この実装の問題点として

  • key:value 形式なので putExtra の key を一々自前で用意しないといけない

  • getExtra する際に渡す Key を間違えると予期しない値を受け取ってしまう可能性がある

などが挙げられると思います.

今回はこの部分を SafeArgs に置き換えて実装していきます.

まずはじめに navigation.xml を作っておきます.

<fragment> タグではなくて<activity> タグで括ります.

<navigation
    ...
    >
  <activity android:name="com.mzkii.dev.navigationactivities.ResultActivity">
    <argument
        android:name="userIds"
        app:argType="integer[]"
        />
    <argument
        android:name="userName"
        app:argType="string"
        />
  </activity>
  ...
</navigation>

この状態でビルドすると ResultActivityArgs が生成されます.

data class ResultActivityArgs(val userIds: IntArray, val userName: String) : NavArgs {
    fun toBundle(): Bundle {
        val result = Bundle()
        result.putIntArray("userIds", this.userIds)
        result.putString("userName", this.userName)
        return result
    }

    companion object {
        @JvmStatic
        fun fromBundle(bundle: Bundle): ResultActivityArgs {
            bundle.setClassLoader(ResultActivityArgs::class.java.classLoader)
            val __userIds : IntArray?
            if (bundle.containsKey("userIds")) {
                __userIds = bundle.getIntArray("userIds")
                if (__userIds == null) {
                    throw IllegalArgumentException("Argument \"userIds\" is marked as non-null but was passed a null value.")
                }
            } else {
                throw IllegalArgumentException("Required argument \"userIds\" is missing and does not have an android:defaultValue")
            }
            val __userName : String?
            if (bundle.containsKey("userName")) {
                __userName = bundle.getString("userName")
                if (__userName == null) {
                    throw IllegalArgumentException("Argument \"userName\" is marked as non-null but was passed a null value.")
                }
            } else {
                throw IllegalArgumentException("Required argument \"userName\" is missing and does not have an android:defaultValue")
            }
            return ResultActivityArgs(__userIds, __userName)
        }
    }
}

さらに, 専用の拡張関数も用意しておきます.

inline fun <reified T> Activity.startActivity(bundleArgs: Bundle) {
  val intent = Intent(this, T::class.java).apply {
    putExtras(bundleArgs)
  }
  startActivity(intent)
}
fun Activity.requireBundle() = intent.extras ?: throw IllegalArgumentException("Bundle() must not be null.")

これで準備は完了です.

Activity を立ち上げるときはこんな感じになります.

startActivity<ResultActivity>(
  ResultActivityArgs(
    userIds = intArrayOf(22, 23, 24),
    userName = "mzkii"
  ).toBundle()
)

ResultActivityArgs#toBundle() を使うと Bundle に引数の値を詰めて返してくれます.

そして, 受け取る側は以下のようになります.

onCreate 以降で使いたいので遅延初期化します.

class SafeargsActivity : AppCompatActivity() {

  private val args by lazy {
    ResultActivityArgs.fromBundle(requireBundle())
  }
  
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    ...
    val userIds = args.userIds
    val userName = args.userName
  }

こうすることで, IDE で補完の恩恵を受けつつ, createIntent の定義がなくなり, かつ間違った key を指定する事もなくなりました.

その一方で,

  • 必要な引数達を navigation.xml に集約して書くことになるので activity のコーディング時に必要な引数がぱっと見て分かりづらくなる
  • 拡張関数 startActivity の引数が Bundle なので ResultActivityArgs 使わなくても結局起動できてしまう
  • コードの自動生成が少なからず走るので, 画面数が増えた時にビルド時間が伸びそう

みたいなデメリットもあるな〜と感じました.

SafeArgs は結構便利なライブラリなので, Navigation 入れる前段階として徐々に導入していくと良いと思います.