Deep linking from Notification - how to pass data back up through the backstack?
N

1

6

In my app, the user can select a category, then select an item within that category to finally view the item details. The standard/forward flow is:

SelectCategoryFragment -> SelectItemFragment -> ViewItemDetailsFragment

On selecting a category, the selectedCatId is passed via a Bundle from SelectCategoryFragment to SelectItemFragment:

    NavController navController = Navigation.findNavController(v);
    Bundle args = new Bundle();
    args.putLong(SelectItemFragment.ARG_CATEGORY_ID, selectedCatId);
    navController.navigate(R.id.action_nav_categories_to_items, args);

SelectItemFragment will then use the getArguments().getLong(ARG_CATEGORY_ID) value to query and display the appropriate items from the selected category.

That works fine. But I am now trying to implement deep linking when the users taps on a Notification, jumping them straight to ViewItemDetailsFragment with a backstack that can take them up to SelectItemFragment, then SelectCategoryFragment.

My problem is that, as described, SelectItemFragment depends on the ARG_CATEGORY_ID argument being passed to it in order to retrieve/display its data. I've read up on deep linking and nested navigation graphs, but don't really know how to pass ARG_CATEGORY_ID with deep linking/backstacks.

Is there a tidy way I can pass data from ViewItemDetailsFragment to SelectItemFragment when the user presses back?

Norvil answered 15/6, 2020 at 17:29 Comment(0)
C
4

TLDR: problem might be solved using nested graphs. Refer to this article for more details.

Longer answer

Let's define simple fragment FragmentRed, FragmentGreen and FragmentBlue who inflate simple layouts each with corresponding background color:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/holo_red_dark">

    <TextView
        android:id="@+id/textView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:gravity="center"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

And declare fragment classes as such:

class FragmentRed : Fragment() {

    private val args: FragmentRedArgs by navArgs()

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.red, container, false)
        view.findViewById<TextView>(R.id.textView).text = args.foo.toString()
        return view
    }
}

FragmentGreen and FragmentBlue are copy pasted, but substituted all color texts with corresponding color texts, i.e. FragmentRedArgs -> FragmentBlueArgs, R.layout.red -> R.layout.blue.

Let's declare main activity layout as such:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:id="@+id/content"
    android:layout_height="match_parent">

    <fragment
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:navGraph="@navigation/main_graph" />
</FrameLayout>

Where main_graph is:

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/main_graph"
    app:startDestination="@id/fragment_red">

    <fragment
        android:id="@+id/fragment_red"
        android:name="com.playground.FragmentRed">
        <argument
            android:name="foo"
            android:defaultValue="0"
            app:argType="integer" />
        <action
            android:id="@+id/action_fragment_red_to_fragment_green"
            app:destination="@id/fragment_green" />
    </fragment>

    <navigation
        android:id="@+id/secondLevel"
        app:startDestination="@id/fragment_green">

        <fragment
            android:id="@+id/fragment_green"
            android:name="com.playground.FragmentGreen">
            <argument
                android:name="bar"
                android:defaultValue="0"
                app:argType="integer" />
            <action
                android:id="@+id/action_fragment_green_to_fragment_blue"
                app:destination="@id/fragment_blue" />
        </fragment>

        <fragment
            android:id="@+id/fragment_blue"
            android:name="com.playground.FragmentBlue">
            <argument
                android:name="zar"
                android:defaultValue="0"
                app:argType="integer" />
        </fragment>
    </navigation>

</navigation>

Now inside MainActivity let's spawn a new notification, passing in arguments for each of fragments: "foo" (red) - 1, "bar" (green) - 2, "zar" (blue) - 3.

Our expectation is upon clicking on notification to open Blue screen with text 3, upon back click see Green screen with 2 and another click should bring Red screen with 1 on the screen:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val navController = findNavController(R.id.nav_host_fragment)
        val pendingIntent = navController.createDeepLink()
            .setGraph(R.navigation.main_graph)
            .setDestination(R.id.fragment_blue)
            .setArguments(bundleOf("foo" to 1, "bar" to 2, "zar" to 3))
            .createPendingIntent()

        createNotificationChannel() // outside of the scope of this answer
        val builder = NotificationCompat.Builder(this, "my_channel")
            .setContentTitle("title")
            .setContentText("content text")
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
            .setContentIntent(pendingIntent)
            .setSmallIcon(R.drawable.android)
            .setAutoCancel(true)
            .setChannelId("channelId")

        with(NotificationManagerCompat.from(this)) {
            notify(100, builder.build())
        }
    }

}

Here's the actual behavior on device:

enter image description here

Clarke answered 19/6, 2020 at 18:27 Comment(2)
Thanks, I'll give this a try. So, basically, the Bundle passed into setArguments() when creating the PendingIntent is automatically passed up the backstack?Norvil
I believe so. But please don't ask what happens if two different fragments have same name for input argument.Clarke

© 2022 - 2024 — McMap. All rights reserved.