ticktakclockの日記

技術ポエムを綴ったりします。GitHub idも同じです (@ticktakclock)

Navigation SafeArgsを使って画面遷移でパラメータを渡す

こんにちは、tkyです。

前回はBottomNavigationとNavigationをあわせた画面遷移を作りましたが、パラメータを渡せないと実務では利用しづらいケースが多いと思います。

↓前回記事↓ ticktakclock.hatenablog.com

今回はSafe Argsの機能を使って次の画面にパラメータを渡すような実装を試してみたいと思います。

Home画面、Dashboard画面という単語が出てきますが、前回作ったアプリの画面をそのまま利用しているので前回記事を一読しておくと理解しやすいと思います。

今回の実装は前回のGitHubに追記しています。

github.com

Safe Argsできて何が嬉しいのか

ここで解決できることは、今までFragmentにnewInstance()でFragmentのインスタンスを作っていた関数が不要になり

companion object{
    fun newInstance(editName: String): DashboardFragment {
        val bundle = Bundle()
        bundle.putString("DETAIL_NAME", editName)
        return DashboardFragment().apply {
            arguments = bundle
        }
    }
}

このように記述できることにあると思います。

引数を含めたDestinationが型になるためActivityとFragmentを意識する必要がなくなる、contextも不要になることが大きなメリットです。

// 引数を含めたDestination
val destination =
    HomeFragmentDirections.actionNavigationHomeToNavigationDashboard(textValue)
findNavController().navigate(destination)

githubは前回と同じリポジトリに追記しています。

gradleへ追記

Safe Argsを利用するためにはNavigationの依存追加の他に、Project.gradleに以下を追加する必要があります。

    buildscript {
        repositories {
            google()
        }
        dependencies {
            def nav_version = "2.1.0"
            classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
        }
    }
    

また、app.gradleに以下のプラグインを追加します。Kotlinだけのプロジェクトの場合の例です。

apply plugin: "androidx.navigation.safeargs.kotlin"

Java/Kotin混合プロジェクトの場合 apply plugin: "androidx.navigation.safeargs" となります。

Argumentの定義

Navigation Design上で確認していきましょう。HOME画面→Dashboard画面の遷移でDashboard画面でパラメータを受け取りたい場合、

DashboardDestinationをクリックしてAttributesの中にあるArgumentsの+ボタンをクリックします。

f:id:ticktakclock:20200202152649p:plain:w200

するとどのような引数を追加するか入力するダイアログが出てきますので、NameとTypeだけ入力してあとは何もせずでAddします。

  • Name: editName
  • Type: String
  • Array: non checked
  • Nullable: non checked
  • Default Value: empty

これで完了です。xml上でも引数が追加されていることが確認できます。

<fragment
    android:id="@+id/navigation_dashboard"
    android:name="com.github.ticktakclock.bottomnavigationsample.ui.dashboard.DashboardFragment"
    android:label="@string/title_dashboard"
    tools:layout="@layout/fragment_dashboard">
    <!-- 中略 -->
    <argument
        android:name="editName"
        app:argType="string" />
</fragment>

argTypeについてはSerializableやParcelableなどの型も対応しているので独自データクラスなども対応できそうですね。良かったです。

詳細はこのページを見るとより深く理解できそうです。

遷移元処理

home画面にテキスト入力できるようにレイアウトを変更して、画面遷移時テキストの内容を遷移先に渡してみましょう。

HomeFragmentDirections.actionNavigationHomeToNavigationDashboard(editName: String) という遷移情報が自動で作られていることがわかります。

ここで作られたDestinationで画面遷移することになります。

val button: Button = root.findViewById(R.id.button)
button.setOnClickListener {
    val editText: EditText = root.findViewById(R.id.editText)
    val textValue = editText.text.toString()
    // 以前はリソースIDを直接指定していたが今回は引数を含めたDestinationを指定
    val destination =
        HomeFragmentDirections.actionNavigationHomeToNavigationDashboard(textValue)
    findNavController().navigate(destination)
//            findNavController().navigate(R.id.action_navigation_home_to_navigation_dashboard)
}

遷移先処理

Home画面から送った引数をDashboard画面で受け取ってみましょう。2種類方法があります(やってることは同じです)。

1つめは DashboardFragmentArgs.fromBundle()を使ってargumentsからDashboardFragmentArgsインスタンスを取得するやり方です。

arguments?.let {
    val args = DashboardFragmentArgs.fromBundle(it)
    val argsText: TextView = root.findViewById(R.id.text_args)
    argsText.text = args.editName
}

2つめは by navArgs() プロパティデリゲートを使ってDashboardFragmentArgsインスタンスを取得するやり方です。

private val args: DashboardFragmentArgs by navArgs()

override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {

val argsText: TextView = root.findViewById(R.id.text_args)
argsText.text = args.editName
    return root
}

1つ目はargumentsがBundle?型なので一度Nullチェックをする必要があります。nullだったときのことを考える必要があります。 この場合引数は必ず存在することが前提なのでBundleの状態不正としてIllegalStateExceptionをThrowして実装者に気づかせるのが最善かと思われます。

2つ目の方が公式ドキュメントでも解説されていたやり方となります。nullチェックする必要がなく、こちらのほうが記述量も少なくなって楽ですね。

navArgs()は何をしているのか

実は navArgs() デリゲートの中を見ると1つ目とやっていることは何も変わりませんでした。ライブラリ側にチェック処理を任せられるので実装ミスもなくなる観点からnavArgs()を使ったほうがメリットが大きそうです。

@MainThread
inline fun <reified Args : NavArgs> Fragment.navArgs() = NavArgsLazy(Args::class) {
    arguments ?: throw IllegalStateException("Fragment $this has null arguments")
}

この状態でHomeFragmentで findNavController().navigate(R.id.action_navigation_home_to_navigation_dashboard) で引数を与えないリソースIDのみで画面遷移しようとすると想定通り IllegalStateException が吐かれました

E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.github.ticktakclock.bottomnavigationsample, PID: 9503
    java.lang.IllegalStateException: Fragment DashboardFragment{d5e17c6 #1 id=0x7f08006c} has null arguments
        at com.github.ticktakclock.bottomnavigationsample.ui.dashboard.DashboardFragment$$special$$inlined$navArgs$1.invoke(FragmentNavArgsLazy.kt:42)
        at com.github.ticktakclock.bottomnavigationsample.ui.dashboard.DashboardFragment$$special$$inlined$navArgs$1.invoke(Unknown Source:0)
        at androidx.navigation.NavArgsLazy.getValue(NavArgsLazy.kt:44)
        at androidx.navigation.NavArgsLazy.getValue(NavArgsLazy.kt:34)

まとめ

  • Safe Argsでを使うと Activity.createIntent(), Fragment.newInstance() といった関数が不要になる
  • arg typsはSerializable, Parcelableも渡せるので独自クラスでも対応できる
  • navArgs() を使ってパラメータ取得すべし

Navigationでスマートな画面遷移を実現していきましょう〜!

あとは公式Doc見るのがよいです

developer.android.com