Deeplink empty String Argument

Viewed 293

I'm using the Jetpack Navigation as a navigation solution in my app.

I'm facing an inconsistent behaviour when using the generated safe args as opposed to the deeplink strategy.

Say I have following navigation graph:

<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"
    app:startDestination="@+id/home_dest">

    <fragment
        android:id="@+id/home_dest"
        android:name="com.example.android.codelabs.navigation.HomeFragment"
        android:label="@string/home"
        tools:layout="@layout/home_fragment" >
        <action
            android:id="@+id/action_home_dest_to_deeplink_dest"
            app:destination="@id/deeplink_dest" />
    </fragment>

    <fragment
        android:id="@+id/deeplink_dest"
        android:name="com.example.android.codelabs.navigation.DeepLinkFragment"
        android:label="@string/deeplink"
        tools:layout="@layout/deeplink_fragment">

        <argument
            android:name="myarg"
            app:argType="string" />

        <deepLink app:uri="example://my-deelink-frag?myArg={myarg}" />
    </fragment>
</navigation>

I have defined 2 ways to navigate to the DeepLinkFragment:

  • Navigation action with id action_home_dest_to_deeplink_dest
  • A deeplink example://my-deelink-frag?myArg={myarg}

In the DeepLinkFragment I'm using the navArgs() function to de-serialize the incoming Bundle and show a simple Toast message.

class DeepLinkFragment : Fragment() {

    private val args : DeepLinkFragmentArgs by navArgs()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        Toast.makeText(requireContext(), args.myarg, Toast.LENGTH_SHORT).show()
    }
}

When navigating with the generated safe args I can sent an empty string as the value for myArg. No problem.

findNavController().navigate(HomeFragmentDirections.actionHomeDestToDeeplinkDest(""))

However, when navigating with an empty string as argument via the deeplink way:

val arg = ""
findNavController().navigate("example://my-deelink-frag?myArg=${arg}".toUri())

I get following stacktrace:

 Caused by: java.lang.IllegalArgumentException: Required argument "myarg" is missing and does not have an android:defaultValue
    at com.example.android.codelabs.navigation.DeepLinkFragmentArgs$Companion.fromBundle(DeepLinkFragmentArgs.kt:29)
    at com.example.android.codelabs.navigation.DeepLinkFragmentArgs.fromBundle(Unknown Source:2)
    at java.lang.reflect.Method.invoke(Native Method) 
    at androidx.navigation.NavArgsLazy.getValue(NavArgsLazy.kt:52) 
    at androidx.navigation.NavArgsLazy.getValue(NavArgsLazy.kt:34) 
    at com.example.android.codelabs.navigation.DeepLinkFragment.getArgs(DeepLinkFragment.kt:30) 
    at com.example.android.codelabs.navigation.DeepLinkFragment.onViewCreated(DeepLinkFragment.kt:34) 
    at androidx.fragment.app.Fragment.performViewCreated(Fragment.java:2987) 
    at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:546) 
    at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:282) 
    at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:2189) 
    at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:2106) 
    at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:2002) 
    at androidx.fragment.app.FragmentManager$5.run(FragmentManager.java:524) 
    at android.os.Handler.handleCallback(Handler.java:938) 
    at android.os.Handler.dispatchMessage(Handler.java:99) 
    at android.os.Looper.loop(Looper.java:233) 
    at android.app.ActivityThread.main(ActivityThread.java:8068) 
    at java.lang.reflect.Method.invoke(Native Method) 
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:631) 
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:978) 

Which is coming from the generated DeeplinkFragmentArgs code

public data class DeepLinkFragmentArgs(
  public val myarg: String = ""
) : NavArgs {
  public fun toBundle(): Bundle {
    val result = Bundle()
    result.putString("myarg", this.myarg)
    return result
  }

  public companion object {
    @JvmStatic
    public fun fromBundle(bundle: Bundle): DeepLinkFragmentArgs {
      bundle.setClassLoader(DeepLinkFragmentArgs::class.java.classLoader)
      val __myarg : String?
      if (bundle.containsKey("myarg")) {
        __myarg = bundle.getString("myarg")
        if (__myarg == null) {
          throw IllegalArgumentException("Argument \"myarg\" is marked as non-null but was passed a null value.")
        }
      } else {
        __myarg = ""
      }
      return DeepLinkFragmentArgs(__myarg)
    }
  }
}

Looking at the code it seems as if an empty string is making the bundle.getString("myarg") return null instead of the empty string?!

As a fix I could add the android:defaultValue.
However I don't think is the right solution.

<argument
    android:name="myarg"
    android:defaultValue=""
    app:argType="string" />

Is this a known limitation when using deeplinks?

2 Answers

Not sure if this fully answers your question, but what you've done all looks correct. Some related notes below based on my own experience.

INTERNAL LINKS

I would maybe use SafeArgs for navigation internal to the app, or just call findNavController.navigate as in this code of mine. Personally I'm not a big fan of more Android XML markup.

EXTERNAL LINKS

If you want links to work from sources such as emails then the deep link option is the best fit, but you will also then need to validate input. As you seem to indicate, an empty default value on its own may not be sufficient.

So before using DeepLinkFragmentArgs I would ensure that you get the desired control. Eg in the past I have sometimes found it useful to do validation before allowing navigation to proceed - eg to receive deep links first in a Forwarder Activity. This meant I could prevent deep links from executing while a Chrome Custom Tab is topmost (since this caused problems such as recreating the main activity).

MOBILE DEEP LINKS

Incidentally I've found that custom URI scheme deep links may not be allowed by email clients such as gmail, so my sample uses Android https applinks, just in case that is of any interest to you.

One probably cannot pass an empty string as argument; for string one would usually pass "@null" as default value (which also requires a nullable field); but it could as well be string "default", if you don't want to use any null values. If something may appear inconsistent, you may not have referenced the default nav-graph XML in AndroidManifest.xml, which merely defines the entry-point for the deep-linking. Also, version 2.5.0-alpha01 may be required, in order to build with the current AGP 7.1.1. There's no right or wrong, but it's convenient to let the NavController handle that - and generating code is preferred, over writing code.

At least in common HTTP, %02%03 would be an empty query-string parameter (yet untested).

Related