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 入れる前段階として徐々に導入していくと良いと思います.