Skip to content

Commit b67f3bf

Browse files
authored
ref(android): Add NavScopes (JAVA-785) (#6304)
Part 2 of our Nav3 support for multi-Activity and multi-Fragment applications Introduces NavScopes, which lets BackStackObserver talk to a facade that knows when to swizzle between the real, shared IScope used by all SentryNavEffects vs a per-instance staging area. Effects that later gain the right to update the shared IScope can publish their staged values.
1 parent f3025c6 commit b67f3bf

7 files changed

Lines changed: 1399 additions & 0 deletions

File tree

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package io.sentry.compose.navigation3.lifecycle
2+
3+
import io.sentry.IScope
4+
import io.sentry.ITransaction
5+
import io.sentry.PropagationContext
6+
import io.sentry.protocol.App
7+
import io.sentry.protocol.Contexts
8+
9+
/**
10+
* An [INavScope] implementation that writes to and reads from [delegate] only when [owner] holds
11+
* the nav lease; otherwise it falls back to [stagedNavData].
12+
*/
13+
internal class NavScope(
14+
private val delegate: IScope,
15+
private val stagedNavData: StagedNavData,
16+
private val coordinator: NavLeaseCoordinator,
17+
private val owner: Any,
18+
) : INavScope {
19+
20+
private val hasLease: Boolean
21+
get() = coordinator.hasLease(owner)
22+
23+
override var screen: String?
24+
get() = if (hasLease) delegate.screen else stagedNavData.screen
25+
set(value) {
26+
stagedNavData.stageScreen(value)
27+
if (hasLease) {
28+
stagedNavData.publishScreen(delegate)
29+
}
30+
}
31+
32+
override fun setNavigationContext(value: Any?) {
33+
stagedNavData.stageNavigationContext(value)
34+
if (hasLease) {
35+
stagedNavData.publishNavigationContext(delegate)
36+
}
37+
}
38+
39+
/**
40+
* Copies the whole of [IScope.getContexts] in order to return a copy of its [App].
41+
*
42+
* [App]'s copy constructor isn't public, so an `App` can't be copied on its own. Going through
43+
* [Contexts] deep-copies every context on the scope and discards all of it but the `App`.
44+
*
45+
* Called once per navigation that changes the top of the back stack, so the cost is bounded.
46+
* Don't add call sites without reading [INavScope.app] first.
47+
*/
48+
override val app: App?
49+
get() = Contexts(delegate.contexts).app
50+
51+
/**
52+
* The transaction bound to the scope. Without the lease, another owner's nav transaction isn't
53+
* reported, so [owner]'s attempt to start a nav transaction reaches [NavScopes.startTransaction],
54+
* which declines it and defers the propagation context rotation it implies. Non-nav transactions
55+
* (e.g., the host's) are always reported, so they still block nav transactions as usual.
56+
*/
57+
override val transaction: ITransaction?
58+
get() =
59+
delegate.transaction?.takeUnless { current ->
60+
!hasLease && coordinator.isNavTransaction(current)
61+
}
62+
63+
override fun bindTransaction(transaction: ITransaction) {
64+
if (!hasLease) {
65+
return
66+
}
67+
delegate.withTransaction { current ->
68+
if (current == null) {
69+
delegate.transaction = transaction
70+
}
71+
}
72+
}
73+
74+
override fun unbindTransaction(transaction: ITransaction) {
75+
// Coordinator is called before clearTransaction() because the latter notifies the host app's
76+
// scope observer(s), which could throw. Clearing the transaction first would cause the
77+
// coordinator to retain a stale transaction.
78+
coordinator.releaseTransaction(owner, transaction)
79+
delegate.withTransaction { current ->
80+
if (current === transaction) {
81+
delegate.clearTransaction()
82+
}
83+
}
84+
}
85+
86+
override fun clearFinishedTransaction() {
87+
if (!hasLease) {
88+
return
89+
}
90+
delegate.withTransaction { current ->
91+
if (current?.isFinished == true) {
92+
delegate.clearTransaction()
93+
}
94+
}
95+
}
96+
97+
override fun rotatePropagationContext() {
98+
if (hasLease) {
99+
delegate.withPropagationContext { delegate.propagationContext = PropagationContext() }
100+
} else {
101+
stagedNavData.deferPropagationContextRotation()
102+
}
103+
}
104+
}
105+
106+
/**
107+
* The subset of the [IScope] interface used to record navigation data.
108+
*
109+
* Protects against accidental misuse of what would otherwise be a partially-implemented `IScope`
110+
* interface. New operations against the underlying `IScope` instance must be added here. Operations
111+
* not directly delegable to `IScope` should ***not*** be added.
112+
*/
113+
internal interface INavScope {
114+
115+
var screen: String?
116+
117+
/**
118+
* A copy of the scope's app context, if any.
119+
*
120+
* A copy rather than the live instance, because callers mutate the result before binding it to a
121+
* transaction, and mutating the scope's own [App] would corrupt shared scope state.
122+
*
123+
* Implementations may have to copy more than the [App] to produce it, so treat this as expensive:
124+
* read it once where it's needed rather than per frame or in a loop. See [NavScope.app].
125+
*/
126+
val app: App?
127+
128+
/**
129+
* The transaction bound to the scope, if any, as far as starting a nav transaction is concerned:
130+
* see [NavScope.transaction].
131+
*/
132+
val transaction: ITransaction?
133+
134+
/** Binds [transaction] to the scope if no transaction is bound to it. */
135+
fun bindTransaction(transaction: ITransaction)
136+
137+
/**
138+
* Unbinds [transaction] from the scope if it's the bound transaction, and ensures it won't be
139+
* bound to the scope again.
140+
*/
141+
fun unbindTransaction(transaction: ITransaction)
142+
143+
/** Unbinds the scope's transaction if it has finished. */
144+
fun clearFinishedTransaction()
145+
146+
/**
147+
* Sets or, given `null`, clears the value stored under [NAVIGATION_CONTEXT_KEY].
148+
*
149+
* Implementations should clear via [IScope.setContexts] rather than [IScope.removeContexts], as
150+
* the latter doesn't notify scope observers and therefore doesn't persist the removal to disk.
151+
*/
152+
fun setNavigationContext(value: Any?)
153+
154+
/** Replaces the scope's propagation context with a new one. */
155+
fun rotatePropagationContext()
156+
}

0 commit comments

Comments
 (0)