Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android navigation components with deep link: onNewIntent called multiple times

This time I need your help regarding the use of android navigation components with deeplink.

I have been following this documentation and the connection between fragment and deeplink is working fine.

The problem comes in regards to the activity that is receiving the deeplink. In my case, I set the android:launchMode="singleTask"

<activity android:name=".features.welcome.WelcomeActivity"
    android:launchMode="singleTask">
     <nav-graph android:value="@navigation/welcome_nav_graph" />
</activity>

override fun onNewIntent(intent: Intent?) {
    super.onNewIntent(intent)
    Timber.d("onNewIntent: $intent with activity: $this")
    navController.handleDeepLink(intent)
}

With this configuration I noticed a couple of weird behaviours:

WelcomeActivity receives onNewIntent call two times every time I click the deeplink. Having even sometimes new instances of that activity created.. like

1_ object1-onNewIntent

2_ object1-onNewIntent

3_ object2-onCreate

Here you have some logs:

First launch

onCreate: Intent { flg=0x10000000 cmp={applicationId}/{package}.WelcomeActivity } with activity: {package}.WelcomeActivity@4adbef0

Open deep link

onNewIntent: Intent { act=android.intent.action.VIEW cat=[android.intent.category.BROWSABLE] dat=https://{depp_link}… flg=0x10010000 cmp={applicationId}/{package}.WelcomeActivity (has extras) } with activity: {package}.WelcomeActivity@4adbef0

onNewIntent: Intent { act=android.intent.action.VIEW cat=[android.intent.category.BROWSABLE] dat=https://{depp_link}... flg=0x1001c000 cmp={applicationId}/{package}.WelcomeActivity (has extras) } with activity: {package}.WelcomeActivity@4adbef0

onCreate: Intent { act=android.intent.action.VIEW cat=[android.intent.category.BROWSABLE] dat=https://{depp_link}... flg=0x1001c000 cmp={applicationId}/{package}.WelcomeActivity (has extras) } with activity: {package}.WelcomeActivity@b77c6b

Kill the app and open deep link

onCreate: Intent { act=android.intent.action.VIEW cat=[android.intent.category.BROWSABLE] dat=https://{depp_link}... flg=0x10018000 cmp={applicationId}/{package}.WelcomeActivity (has extras) } with activity: {package}.WelcomeActivity@b78f4df

onNewIntent: Intent { act=android.intent.action.VIEW cat=[android.intent.category.BROWSABLE] dat=https://{depp_link}... flg=0x1001c000 cmp={applicationId}/{package}.WelcomeActivity (has extras) } with activity: {package}.WelcomeActivity@b78f4df

onCreate: Intent { act=android.intent.action.VIEW cat=[android.intent.category.BROWSABLE] dat=https://{depp_link}... flg=0x1001c000 cmp={applicationId}/{package}.WelcomeActivity (has extras) } with {package}.WelcomeActivity@dfe87b2

UPDATE:

1 -It seems launch mode has nothing to do with this issue. I noticed the same with default launch mode.

2- navController.navigate(intent.dataString.toUri()) seems to work fine. So I guess the problem is navController.handleDeepLink(intent).

like image 992
Leandro Ocampo Avatar asked May 22 '20 09:05

Leandro Ocampo


People also ask

What is deep link in navigation component?

In Android, a deep link is a link that takes you directly to a specific destination within an app. The Navigation component lets you create two different types of deep links: explicit and implicit.

What is Android deep linking?

A deep link is a URL that navigates to a specific destination in your app. When you click a deep link, Android: Opens the user's preferred app that can handle the link, if it's available. If the preferred app isn't available, it opens the only app that can handle the link.

How do you use Onnewintent?

This is called for activities that set launchMode to "singleTop" in their package, or if a client used the FLAG_ACTIVITY_SINGLE_TOP flag when calling startActivity(Intent). If you set to single top, the activity will not be launched if it is already running at the top of the history stack.

What is deep linking in Android?

Deep Linking in Android. Deep Linking is a methodology for launching a native mobile application via a link. It consists of a unique URI (Uniform Resource Identifier) that links or matches to a specific location within a mobile app.

How to launch a specific activity once a user clicks on Deeplink?

Depending on your requirement you can launch the specific Activity once a user clicks on the deep link. It is important to note that once a user clicks on a deep link, the Android Activity will launch on top of the current visible Activity (In case if app is open).

Does AGP support manifest placeholders in deep link Uri's?

With this fix, AGP supports manifest placeholders in deep link URI's scheme, host, and path. There is still no valid solution for it. Please star these two issues for faster resolving (I don't think, that it will help, but maybe...):


3 Answers

Testing different changes, I came to the conclusion that "navController.handleDeepLink(intent)" is causing this weird behaviour.

This is what I tried:

I removed the deepLink from the navigation, and the deep link was working just fine (I have added deepLink manually) with a normal behaviour: using singleTask, if the activity is already created, then onNewIntent is called only once. If the activity is not created, then onCreate is called.

An extra problem with this is that navController.handleDeepLink(intent) will be called automatically in onCreate (you can check that in the javadocs). When onNewIntent is called, you need to call navController.handleDeepLink(intent).

I decided to try "navigate(Uri deepLink)" from NavController and I see that is working as expected (behaviour described in the first paragraph). Having this alternative method, I decided to make some changes:

class WelcomeActivity : AppCompatActivity(){

      private val navController by lazy { findNavController(R.id.nav_host_fragment) }

      private var deepLinkData: Uri? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Timber.d("onCreate: $intent  with activity: $this")
        val data = intent.data
        intent.data = null
        setContentView(R.layout.activity_welcome)
        handleDeepLink(data)
    }

    override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
        Timber.d("onNewIntent: $intent with activity: $this")
        setIntent(intent)
        val data = intent?.data
        handleDeepLink(data)
    }

    private fun handleDeepLink(uri: Uri?) {
        //TODO: there is an issue that will cause onNewIntent to be called twice when the activity is already present.
        if (uri != null && deepLinkData.toString() != uri.toString() && navController.graph.hasDeepLink(uri)) {
            //possible deep link for LoginFragment
            deepLinkData = uri
            navController.navigate(uri)
        }
    }

}

It is important to notice this block of code in onCreate:

val data = intent.data
intent.data = null

The reason for this is because if I need to prevent "navController.handleDeepLink(intent)" to be called as it will be called automatically if that information is present, causing the weird behaviour.

navController.graph.hasDeepLink(uri) will help you to see if your graph can handle that uri. If you do not use it, then "navigate(Uri deepLink)" will throw an exception.

Hope it can help you if you are running into the same problem. If you have more insights on this, feel free to leave some comments.

like image 196
Leandro Ocampo Avatar answered Oct 08 '22 08:10

Leandro Ocampo


When callback on onNewIntent arrive for the first time, just setup the flag intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); and pass mutated intent to handleDeepLink(intent);

This flag eliminated the second arrive of onNewIntent callback due to reattaching to existing Activity (with full reconstruction of backstack trace to your desired deep link destination) instead of launching new Activity.

Details is in the source code of handleDeepLink method.

like image 24
A. Petrov Avatar answered Oct 08 '22 07:10

A. Petrov


When an implicit deeplink is clicked the FLAG_ACTIVITY_NEW_TASK is set with intent. And as per the documentation the backstack will be recreated to be in a good state. See the documentation here Implicit deep link.

If you do not want this behavior and do not want to change the intent flags you can handle the deeplink yourself rather than passing it to Navigation components. So if you are using AdvancedNavigationSample you need to remove handledeeplink and use findNavController().navigate() to the direction.

Also remember to set the launch mode for launcher activity as singleTask to receive new intents on clicking deepink or notifications.

  1. Replace NavController.handledeeplink() in sample with
fun BottomNavigationView.navigateDeeplink(
    navGraphIds: List<Int>,
    fragmentManager: FragmentManager,
    containerId: Int,
    uri: Uri
) {
    navGraphIds.forEachIndexed { index, navGraphId ->
        val fragmentTag = getFragmentTag(index)

        // Find or create the Navigation host fragment
        val navHostFragment = obtainNavHostFragment(
            fragmentManager,
            fragmentTag,
            navGraphId,
            containerId
        )
        // Handle deeplink
        val canHandleDeeplink = navHostFragment.navController.graph.hasDeepLink(uri)

        if (canHandleDeeplink) {
            if (selectedItemId != navHostFragment.navController.graph.id) {
                selectedItemId = navHostFragment.navController.graph.id
            }
            navHostFragment.lifecycleScope.launchWhenResumed {
                // Wait for fragment to restore state from backStack
                // otherwise navigate will be ignored
                // Ignoring navigate() call: FragmentManager has already saved its state
                navHostFragment.navController.navigateOnce(uri)
            }
        }
    }
}

  1. Store intent.data in a variable and set it to null so that navigation component can not handle it. Also do the same in onHandleNewIntent
  2. pass this deeplink to bottom navigation to handle. and if the app is already running you can directly navigate to destination using the above function.

The complete solution is implemented at this sample.

Pros:

  1. You can directly handle the notification clicks the same way. Create a deeplink from the actions and pass it in intent.data in PendingIntent.
  2. You can validate and change the deeplink according to your need. like if user is not logged it go directly to login activity or else move to specified destination.
like image 31
beetlestance Avatar answered Oct 08 '22 09:10

beetlestance