Search code examples
androidandroid-architecture-componentsandroid-architecture-navigationandroid-deep-linkandroid-navigation-graph

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


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?


Solution

  • 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