In `Nav3`, when I handle `onNewIntent` deeplink, l...
# compose
u
In
Nav3
, when I handle
onNewIntent
deeplink, like this
Copy code
class MainActivity {
	private val pendingDeeplink = MutableStateFlow<Deeplink?>(null)

	override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        val deeplink = resolveIntent(intent)
        pendingDeeplink.value = deeplink
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    	setContent {
    		val backStack = rememberNavBackStack2<SubscriberNavKey>(MainKey) <---
	        LaunchedEffect(pendingDeeplink) {
	            pendingDeeplink
	                .collect { deeplink ->
	                    applyDeeplink(backstack, deeplink) <----
	                    pendingDeeplink.value = null
	                }
	        }
    		NavDisplay bla bla 
    	}
    }
}
If the app was on
MainKey
, and then went to background -- basically the app is running already, i.e. its not killed -- and then I apply the deeplink which say pushes some screen -- I always see
MainKey
screen for a split second, no matter if the next screen has transition animation or not, it's always seen briefly I'd assume it's due to the way I apply the deeplink, via
LaunchedEffect
which runs after composition. Is there a way to not have the
MainKey
be seen and have the deeplink applied "sooner" somehow?
k
try DisposableEffect
u
how do I collect a flow without coroutine? or are you saying to not use flow?
v
I guess I don't see why you're using a flow in the first place. Why not call
applyDeeplink
in
::onNewIntent
?
k
☝️ extract the backStack outside
u
I'm just prototyping so I'm not set with this pattern
hmm I could .. but then I need to save it my self .. which I guess should be warranted now
k
or instead of the MutableStateFlow use a regular mutableState + DisposableEffect
and
LaunchedEffect(pendingDeeplink)
has no sense. MutableStateFlow is not changed
u
thats just in case instance were to change, same as
collectAsState
works
so you're suggesting something like this?
Copy code
class MainActivity {
	private val backstack = StateListWhatever<NavKey>()

	override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        val deeplink = resolveIntent(intent)
        applyDeeplink(backstack, deeplink)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    	setContent {
    		
    		NavDisplay bla bla(backstack)
    	}
    }
}
1
+ saveState handling?
v
You could even declare the state via
lateinit
and defer assignment to composition. This would crash if composition never happened prior to
onNewIntent
, but I'm not sure that's possible through non-programmatic means.
u
I'm not sure what's that supposed to solve?
v
That would allow you to keep using
rememberNavBackStack2
.
Untitled.cpp
u
I see so to keep it in the field
I think I like youre original suggestion better, to simply take it over myself
Copy code
class MainActivity {
	private lateinit var backstack: NavBackStack<NavKey>()

	override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        val deeplink = resolveIntent(intent)
        applyDeeplink(backstack, deeplink)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        if (savedInstanceState != null) {
        	backstack = savedInstanceState.getBackStack(KEY)
        } else {
        	backstack = NavBackStack<NavKey>(MainKey)
        }
    	setContent {
    		NavDisplay bla bla(backstack)
    	}
    }
}
v
The issue you may run into is that savedstate (android) is different from saveable (compose)
u
why? I'm looking at what the remembernavbackstackdoes
Copy code
rememberSerializable(
        serializer = NavBackStackSerializer(elementSerializer = NavKeySerializer())
    ) {
        NavBackStack(*elements)
    }
I'd imagine its just to use kotlinx.serialization, into string, pass that into the android savedstate as a blob
Copy code
override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putString(KEY, Json.encodeblabla(backstack, NavBackStackSerializer())
    }
v
It also supports Android specific types as well (
@Parcelable
,
IBinder
, and
SparseArray
)
I personally use
rememberSaveable
for my backstack with entries being annotated with
Parcelable
. YMMV
u
I know but specifically this, doesnt it always use json? since this builtin stuff uses
KSerializer
?
v
rememberSerializable
supports (the aforementioned) platform-specific types
u
> I personally use
rememberSaveable
for my backstack with entries being annotated with
Parcelable
. btw why? doesn't that pull in android dependency when defining navkeys? or do you not mind? If figured they went the
@Serializable
route out of the box for this exact reason
v
I only target Android so there's no dependency issue. As for why,
rememberNavBackStack
uses reflection or requires repeating all entry types via
SavedStateConfiguration
.
Parcelable
has no such issue
u
reflection huh? whats the
@Serializable
requirement on keys then for?
anyways the tldr; is you as well don't use the builtin
rememberNavBackStac()
, right?
> anyways the tldr; is you as well don't use the builtin
rememberNavBackStac()
, right? I just use
Copy code
val backStack = rememberSaveable { mutableStateListOf<Any>(Home) }
u
I see so it's not mandatory somehow .. means you can also skip the
@Serializable
on keys as well right
v
You own the backstack, it can be whatever you want.
Copy code
@Parcelize
data object Home : Parcelable
u
yea, its just strange mixing reflection and @Serializable, totally unexpected
I wonder now .. that reflection is only applied at save/restore time, so very infrequently.. is it worth the new plugin? chances are you already have the kotlinx.serialization applied
i
Why are you overriding
onNewIntent
at all when we already talked about how you can receive that signal inside composition where your back stack is already defined? You don't need any of this MutableStateFlow+collect stuff https://kotlinlang.slack.com/archives/CJLTWPH7S/p1775346591629329?thread_ts=1775315820.880149&amp;cid=CJLTWPH7S
u
Hmm, well the prior convo was about, or atleast I read it as having per nav entry callback But now I think, and correct me if wrong, you mean to only have a single onNewIntent callback but declared from within
setContent
, i.e. where the
backstack
reference is in scope, so I can still use the
rememberNavBackStack()
Am I right?
i
The same pattern of getting the onNewIntent into the composable that needs to process it applies to both situations, yes
u
Okay now it clicked, thank you!
Okay so I tried it, api is nicer, however the original effect is still there, is there no way around it? Or it is what it is? (Even if I turn off the transition) See video
@Ian Lake For posterity, unfortunately the
OnNewIntentProvider
registered in composition is not process restore safe (on deeplink). As it's registered later, and can/will miss the
onNewIntent
(race)
Copy code
class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()

        setContent {
            val activity = LocalActivity.current as ComponentActivity
            DisposableEffect(activity) {
                Log.d("Default", "registering onNewIntentProvider")
                val listener = Consumer<Intent> { value ->
                    Log.d("Default", "callback onNewIntent intent=${value.data}")
                }
                activity.addOnNewIntentListener(listener)
                onDispose {
                    activity.removeOnNewIntentListener(listener)
                }
            }
            AppRoot(appStateHelper)
        }
    }

    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        Log.d("Default", "onNewIntent")
    }
}
i.e. when I say go to pay something into
custom tabs
, which are a different process, so my app is then eligible to get killed, so I kill it, and then on redirect/deeplink back to the app's process is started again, however - the odd bit - obviously
activity.onCreate
is called but
intent
there is null - and it immediately calls
onNewIntent
with the proper not null
intent
. And that is a race between
activty.onNewIntent
and the
OnNewIntentProvider
registration in composition, which is a race and most of the time it misses by a lot
Copy code
13:25:23.699  D  onNewIntent
13:25:24.158  D  registering onNewIntentProvider
half a second -- unless I'm doing it wrong
v
Registration being delayed (and not restored) makes sense to me. I can't speak for why the intent deliver pattern is like that, however.
u
Yea it does .. however I'd still expect the
onCreate
carry the intent and onNewIntent not get called here, but yea a race nonetheless