1

I have a nav graph as follows:

<navigation
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main_graph"
    app:startDestination="@id/jobsFragment">

    <fragment
        android:id="@+id/jobsFragment"
        android:name=".JobsFragment"
        android:label="JobsFragment">

        <action
            android:id="@+id/action_jobsFragment_to_newJobDetailsActivity"
            app:destination="@id/newJobDetailsActivity" />

    </fragment>

    <fragment
        android:id="@+id/historyFragment"
        android:name=".HistoryFragment"
        android:label="HistoryFragment" />

    <fragment
        android:id="@+id/profileFragment"
        android:name=".ProfileFragment"
        android:label="ProfileFragment" />

    <activity
        android:id="@+id/newJobDetailsActivity"
        android:name=".NewJobDetailsActivity"
        android:label="activity_new_job_details"
        tools:layout="@layout/activity_new_job_details">

        <!-- This is what I want a reference to -->
        <argument
            android:name="jobId"
            app:argType="string" />

    </activity>

</navigation>

Goal: retrieve the value of newJobDetailsActivity's argument name, jobId, programmatically without needing a reference to an action. With the current API, I can set the jobId through an action without needing to know the string name literal. This is good, but it's not ideal for cases when I want the caller to be able to navigate to a destination without needing to know which component the caller is attached to. This is easier to explain with code. Let's take a look at two scenarios that I'm trying to solve for.

Scenario A

Let's assume I have a RecyclerView.ViewHolder that has some code in it that navigates from a Fragment called JobsFragment to an Activity called NewJobDetailsActivity when clicked on.

itemView.setOnClickListener {
    itemView.findNavController().navigate(
        JobsFragmentDirections.ActionJobsFragmentToNewJobDetailsActivity(jobId)
    )
}

This is great if the RecyclerView.ViewHolder only ever exists in the JobsFragment, but if I want to reuse it from an additional Fragment or Activity in the future, my code flops because the view holder needs to reference JobsFragmentDirections's action in order to pass the jobId argument.

Scenario B

Let's assume I need to create a PendingIntent for a notification, and I want to do so using the Navigation Architecture API.

NavDeepLinkBuilder(context)
    .setGraph(R.navigation.main_graph)
    .setDestination(R.id.newJobDetailsActivity)
    .setArguments(Bundle().apply {
        putString("jobId", jobId)
    })
    .createPendingIntent()

In this case, I don't need a reference to an action, but the situation is actually worse because I can't find a reference to the argument name in the main_graph, so I'm forced to specify it using a string literal. Yuck.

tl;dr: Is anyone aware of a way to improve these two scenarios? Is there a way to programmatically get a reference to an argument from a nav graph?

Dependencies: android.arch.navigation:navigation-fragment-ktx:1.0.0-alpha06, android.arch.navigation:navigation-ui-ktx:1.0.0-alpha06

Plugin: androidx.navigation.safeargs

Ryan
  • 3,414
  • 2
  • 27
  • 34

2 Answers2

1

For the second scenario you can use something like this:

val args = JobsFragmentArgs.Builder(id).build().toBundle()
NavDeepLinkBuilder(context)
    .setGraph(R.navigation.main_graph)
    .setDestination(R.id.newJobDetailsActivity)
    .setArguments(args)
    .createPendingIntent()

For the first scenario, personally, I would recommend passing the on click listener from the fragment/activity that creates the adapter and setting the listener to the view holder on bind. Then you can implement on click differently for other fragments/activities.

Emre Eran
  • 236
  • 2
  • 4
  • Hey Emre. I'm looking to get a reference to the argument without needing a reference to a component, such as a Fragment, in the nav graph. My notification has nothing to do with `JobsFragment`, so I don't want it referenced by the notification. As for the click listener, I realize that passing in a click listener is an alternative solution, but it does add complexity. I'm hoping to avoid the added complexity if possible. – Ryan Oct 10 '18 at 21:44
  • Then I'm sorry, I think there is no way to do that in the current version of navigation since you cannot assign ids to arguments in your nav graph. You can only get destinations, so you have to use a string literal. – Emre Eran Oct 10 '18 at 21:53
  • 1
    By the way sorry for using `JobsFragmentArgs` as an example `NewJobDetailsActivityArgs` would be more appropriate for your question. – Emre Eran Oct 10 '18 at 22:04
  • Ah, yes! That actually works. It's not the prettiest way to get the destination's arguments, but it solves the issues to the scenarios I mentioned above. Thanks! – Ryan Oct 10 '18 at 23:44
1

As Emre mentioned in his answer, there's an args builder that can be called from the generated destination's *Args class which can then be built with the argument(s) and converted to a Bundle. The way to go about it isn't the prettiest, but it accomplishes what I'm trying to achieve. Updated implementations:

Scenario A

itemView.setOnClickListener {
    itemView.findNavController().navigate(
        R.id.newJobDetailsActivity,
        NewJobDetailsActivityArgs.Builder(jobId)
            .build()
            .toBundle()
    )
}

Scenario B

NavDeepLinkBuilder(context)
    .setGraph(R.navigation.main_graph)
    .setDestination(R.id.newJobDetailsActivity)
    .setArguments(
        NewJobDetailsActivityArgs.Builder(jobId)
            .build()
            .toBundle()
    )
    .createPendingIntent()

Update: Tapping the notification with the PendingIntent built with the NavDeepLinkBuilder doesn't actually work. The argument doesn't get passed to the destination Activity. Wondering if this is a bug with the Navigation framework. Tracking this question here.

Ryan
  • 3,414
  • 2
  • 27
  • 34