I’ve mostly implemented androidx compose-navigatio...
# compose
m
I’ve mostly implemented androidx compose-navigation, but am having issues with using
navController
too soon after cold launch. For example, after force stopping the app and then sharing a file (from e.g. Solid Explorer) to my app, I get:
Copy code
java.lang.IllegalArgumentException: Cannot navigate to NavDeepLinkRequest{ uri=<android-app://androidx.navigation/foo?uri=><encoded uri> }. Navigation graph has not been set for NavController androidx.navigation.NavHostController@66e3bed.
    at androidx.navigation.NavController.navigate(NavController.kt:1803)
If I stick in an arbitrary length delay then it all works fine. But how to handle this properly?
Hmm, if I stick the associated
LaunchedEffect
and
DisposableEffect
in the same composable that calls the
NavHost
composable, then it seems to work. But I’d rather not do that (because violates single responsibility principle). So what’s the alternative? Some callback, state flag?
i
It sounds like you are trying to manually handle deep links, which is something NavController will do for you. Consider passing a
mimeType
when you use
navDeepLink
to associate a particular destination with the handling of that type of Intent
m
Thanks Ian. I guess I’m missing something here. The incoming Intent is some generic SEND action from a 3rd party file explorer. So essentially a content uri. Doesn’t the
NavDeepLink
need to have a uri that is typically using my app’s domain? There must be something somewhere that will map a generic SEND intent to a specific deep link uri.
i
Deep links support actions, mime types, OR Uri patterns
m
Thanks for the heads-up. So I kind of got it working using:
Copy code
composable(
    route = "foo",
    deepLinks = listOf(
        navDeepLink {
            action = Intent.ACTION_SEND
            mimeType = MIME_TYPE_ZIP
        },
        navDeepLink {
            action = Intent.ACTION_VIEW
            mimeType = MIME_TYPE_ZIP
        },
    ),
) { backStackEntry ->
    val deepLinkIntent: Intent? = backStackEntry.arguments?.getParcelable(NavController.KEY_DEEP_LINK_INTENT)
    val uri = deepLinkIntent?.clipData?.getItemAt(0)?.uri
    ...
    FooScreen(uri)
}
However, in my case the user can share various zip files to the app, but not all zip files are the same. Depending on the filename (or perhaps zip content), I might want to jump to different parts of the nav graph. So what’s the approach here? To have a kind of routing composable?
i
So by reading the contents of the zip file (something that would have to be done asynchronously to avoid blocking the main thread), you'd then know enough information to redirect users to some other destination based on what the zip file contained? Seems reasonable
m
There’s quite a lot of doubling up of these declarations in the manifest (my app actually handles many more types than this). Any plans to streamline this somehow?
Also, it looks like it’s not possible to specify multiple mimeTypes for the same
navDeepLink
whereas it is possible in an
intent-filter
. Is that intentional?
i
It is intentional that you'd declare multiple
navDeepLink
instances. You can, of course, provide your own wrapper around
navDeepLink
, etc. if you find yourself repeating yourself multiple times - it is just Kotlin code 🙂
m
The idea would be that if navDeepLink allowed multiple mimeTypes, then that would open up the possibility of some Manifest intent-filter generation to build the intent-filters exactly as they are possible now.
i
The API has been around for years and is stable, there's no chance it is going to change now. Just write two of them
Like I said, it is really easy to create a
viewDeepLink
helper method if you find yourself repeating the same action many times for instance - then it is one line per mime type and a lot more clear on how each of the fields interact (each deep link is evaluated separately), something the intent filter APIs do extremely poorly
m
Yeah the mimeType thing is not a big deal, I just wish I didn’t have to declare everything twice (kotlin + XML).
I notice the documentation recommends calling
navController.handleDeepLink()
in
onNewIntent()
though I guess that’s for the legacy navigation library. If also relevant for compose navigation, how best to obtain a reference to navController from there?
i
If you are using the default launchMode (which has always been the recommendation), you don't need that at all
Otherwise you actually want a DisposableEffect that uses the
OnNewIntentProvider
APIs that
ComponentActivity
implements: https://developer.android.com/reference/androidx/core/app/OnNewIntentProvider
m
Thanks Ian. Isn’t it possible that the third party app (that sends the intent) includes a launch flag that overrides whatever launch mode my app uses? Or does the manifest launch mode override that?
i
No, what you set in the manifest is what it is, nothing can override that
👍 1
m
I ended up abandoning the “routing” composable idea, mostly because you need to take care of popping this non-screen composable. Instead I added the deepLinks to the home composable, which seems to work well. One curious thing is I can’t get
ACTION_VIEW
with zip mime type working (the composable is not triggered). Using
ACTION_SEND
works fine however. This
ACTION_VIEW
is sent for example when using Open With from Solid Explorer.
Hmm, there is a problem with using the home route, because after navigating away from home to some other screen and then going back to home, the navBackStackEntry still contains the deep link intent and so it’s important not to process that again. What’s the best way to handle this? Ideally, I want to clear the deep link intent as soon as I’ve processed it.
i
If you're reading the arguments from your ViewModel's
SavedStateHandle
(since all arguments are automatically propagated into that), you could just
remove
the key after you're done processing it
m
Interesting. I wasn’t doing it that way (see above - i.e. just constructing from the backStackEntry argument) but I could change to the viewmodel. I wasn’t aware this is automatically propagated. Presumably to all activity view models, right?
i
No, you'd be looking at the
NavController.KEY_DEEP_LINK_INTENT
at the individual destination level
m
Hmm, so that’s what I’ve been doing up until now. Perhaps the issue stems from declaring all the deep links on home rather than on the appropriate screens where the links navigate to. So when going back to home, the backStackEntry has the same content as before. As a workaround, when processing the backStackEntry deep link, I set the ID on savedStateHandle in activity viewmodel. So whenever processing a backStackEntry deep link, I just make sure that the ID is not the same as the ID value in the savedStateHandle. This seems to work well having tested with “Don’t keep activities” developer setting.
i
The backStackEntry is immutable, but SavedStateHandle is not
m
Unrelated issue regarding order in which system back is processed. My screen has two NavHosts and a NavDrawer. Suppose I navigate in navHost1, then navHost2, then open the nav drawer. The system backs are processed as expected: navDrawer -> navHost2 -> navHost1. However, if the screen is rotated before first back, then the order is navHost2 -> navHost1 -> navDrawer. Is there some way to override this?
i
Where you should be using an if (open) { BackHandler } type pattern and have that code after your NavHosts so that it is an overlay that takes precedence over the NavHosts
m
Thanks Ian. I tried that but it didn’t help and it seems because I’m perhaps misusing
movableContentOf
because when I remove that (and use same content for both orientations) the problem goes away.
Copy code
val navHost1 = remember { movableContentOf(navHost1) }
val navHost2 = remember { movableContentOf(navHost2) }
if (isLandscape()) {
    LandscapeContent(
        appBar = appBar,
        navHost1 = navHost1,
        navHost2 = navHost2,
        modifier = modifier,
    )
} else {
    PortraitContent(
        appBar = appBar,
        navHost1 = navHost1,
        navHost2 = navHost2,
        modifier = modifier,
    )
}
So it turns out I just need to put the
BackHandler
inside the
movableContentOf
(in other words straight after the
NavHost
like you mentioned). And I guess a sensible way to enforce that is by doing something like this.
Copy code
@Composable
fun NavHost(
    navController: NavHostController,
    drawerState: DrawerState,
    startDestination: String,
    modifier: Modifier = Modifier,
    builder: NavGraphBuilder.() -> Unit,
) {
    NavHost(
        navController = navController,
        startDestination = startDestination,
        modifier = modifier,
        builder = builder,
    )
    if (drawerState.isOpen) {
        val scope = rememberCoroutineScope()
        BackHandler {
            scope.launch {
                drawerState.close()
            }
        }
    }
}
933 Views