diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav3/Nav3Activity.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav3/Nav3Activity.kt index 3fad5ffe27..e90e27573a 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav3/Nav3Activity.kt +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav3/Nav3Activity.kt @@ -352,6 +352,16 @@ private fun Nav3SampleApp( } } + val openFragmentScenario: (Nav3FragmentMode) -> Unit = { mode -> + activity?.let { host -> + host.startActivity( + Intent(host, Nav3FragmentsActivity::class.java) + .putExtra(Nav3FragmentsActivity.EXTRA_MODE, mode.name) + .putExtras(Bundle().apply { putNav3SampleConfiguration(configuration) }) + ) + } + } + Scaffold( // Expose test tags as resource IDs so adb/uiautomator can target them. modifier = @@ -472,12 +482,19 @@ private fun Nav3SampleApp( entry { route -> TracedNav3Route(route, selectedScenario) { Nav3RouteWorkEffect(route, routeWorkOptions) - MultiFragmentRoute { - activity?.startActivity( - Intent(activity, Nav3FragmentsActivity::class.java) - .putExtras(Bundle().apply { putNav3SampleConfiguration(configuration) }) - ) - } + FragmentScenarioRoute(route, Nav3FragmentMode.REPLACE, openFragmentScenario) + } + } + entry { route -> + TracedNav3Route(route, selectedScenario) { + Nav3RouteWorkEffect(route, routeWorkOptions) + FragmentScenarioRoute(route, Nav3FragmentMode.DIALOG, openFragmentScenario) + } + } + entry { route -> + TracedNav3Route(route, selectedScenario) { + Nav3RouteWorkEffect(route, routeWorkOptions) + FragmentScenarioRoute(route, Nav3FragmentMode.PAGER, openFragmentScenario) } } entry { route -> @@ -550,18 +567,6 @@ private fun Nav3SampleApp( } } - entry { route -> - TracedNav3Route(route, selectedScenario) { - Nav3RouteWorkEffect(route, routeWorkOptions) - FutureRoute(routeName = "Multipane", scenario = "multipane") - } - } - entry { route -> - TracedNav3Route(route, selectedScenario) { - Nav3RouteWorkEffect(route, routeWorkOptions) - FutureRoute(routeName = "Multiple Stacks", scenario = "multistack") - } - } entry { route -> TracedNav3Route(route, selectedScenario) { Nav3RouteWorkEffect(route, routeWorkOptions) diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav3/Nav3MultiFragment.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav3/Nav3MultiFragment.kt index 1ca11284ee..846344103c 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav3/Nav3MultiFragment.kt +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav3/Nav3MultiFragment.kt @@ -5,7 +5,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.LinearLayout -import androidx.activity.ComponentActivity import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -41,6 +40,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentContainerView import androidx.fragment.app.commit +import androidx.fragment.app.commitNow import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.widget.ViewPager2 import io.sentry.samples.android.R @@ -48,7 +48,7 @@ import io.sentry.samples.android.navigation.common.NavigationSampleConfig /** * Hosts Fragments that each own a Nav3 root (back stack, `SentryNavEffect`, and `NavDisplay`), to - * exercise how separate effects hand off during Fragment transactions: + * exercise how separate effects hand off in three independently selectable scenarios: * * - `replace` + back stack (Products → Checkout), optionally with a slow enter animation; * - a full-screen `DialogFragment` overlay whose host Fragment stays resumed; @@ -58,6 +58,15 @@ import io.sentry.samples.android.navigation.common.NavigationSampleConfig // the NavigationEventDispatcherOwner that NavDisplay needs on the view tree. class Nav3FragmentsActivity : FragmentActivity() { + internal val mode: Nav3FragmentMode + get() = + Nav3FragmentMode.entries.firstOrNull { it.name == intent.getStringExtra(EXTRA_MODE) } + ?: Nav3FragmentMode.REPLACE + + internal companion object { + const val EXTRA_MODE = "io.sentry.samples.android.navigation.nav3.FRAGMENT_MODE" + } + /** When true, Fragment `replace` uses a slow enter animation. */ internal var slowTransitions by mutableStateOf(false) @@ -76,10 +85,9 @@ class Nav3FragmentsActivity : FragmentActivity() { setContent { Nav3SampleTheme { MultiFragmentControls( + mode = mode, slowTransitions = slowTransitions, onSlowTransitionsChanged = { slowTransitions = it }, - onOpenCheckoutSheet = ::openCheckoutSheet, - onOpenPager = ::openPager, ) } } @@ -101,9 +109,22 @@ class Nav3FragmentsActivity : FragmentActivity() { ) if (savedInstanceState == null) { - supportFragmentManager.commit { - setReorderingAllowed(true) - add(R.id.nav3_multi_fragment_container, Nav3RootFragment.products()) + if (mode == Nav3FragmentMode.REPLACE) { + supportFragmentManager.commit { + setReorderingAllowed(true) + add(R.id.nav3_multi_fragment_container, Nav3RootFragment.products()) + } + } else { + // Keep Products beneath the overlay or pager for dismissal and Back handoffs. + supportFragmentManager.commitNow { + setReorderingAllowed(true) + add(R.id.nav3_multi_fragment_container, Nav3RootFragment.products()) + } + if (mode == Nav3FragmentMode.DIALOG) { + openCheckoutSheet() + } else { + openPager() + } } } } @@ -153,6 +174,12 @@ class Nav3FragmentsActivity : FragmentActivity() { } } +internal enum class Nav3FragmentMode(val scenario: Nav3Scenario) { + REPLACE(Nav3Scenario.MULTI_FRAGMENT), + DIALOG(Nav3Scenario.FRAGMENT_OVERLAY), + PAGER(Nav3Scenario.VIEW_PAGER2), +} + /** A Fragment that owns one Nav3 root. Pager pages are child Fragments of [Nav3PagerFragment]. */ class Nav3RootFragment : Fragment() { @@ -170,7 +197,8 @@ class Nav3RootFragment : Fragment() { KIND_PAGE -> pageRoots[args.getInt(ARG_PAGE)] else -> Nav3Route.ProductList } - val opensCheckoutFragment = args.getString(ARG_KIND) == KIND_PRODUCTS + val opensCheckoutFragment = + args.getString(ARG_KIND) == KIND_PRODUCTS && activity.mode == Nav3FragmentMode.REPLACE return nav3RootComposeView( activity = activity, @@ -233,6 +261,7 @@ class Nav3CheckoutSheetFragment : DialogFragment() { return nav3RootComposeView( activity = activity, host = "CheckoutSheet", + title = "Checkout dialog", initialRoute = Nav3Route.Checkout("42"), configuration = activity.configuration, onFinish = ::dismiss, @@ -307,23 +336,36 @@ class Nav3PagerFragment : Fragment() { } @Composable -internal fun MultiFragmentRoute(onOpenFragments: () -> Unit) { +internal fun FragmentScenarioRoute( + route: Nav3Route, + mode: Nav3FragmentMode, + onOpenFragments: (Nav3FragmentMode) -> Unit, +) { RouteScaffold( - routeSpec = Nav3Route.MultiFragment.routeSpec(), - testTagPrefix = nav3TestTag("route_multi_fragment"), + routeSpec = route.routeSpec(), + testTagPrefix = nav3TestTag("route_${mode.name.lowercase()}_fragments"), cardContent = { RouteButton( - "Open fragments Activity", - onClick = onOpenFragments, - testTag = nav3TestTag("multi_fragment_open_fragments"), + when (mode) { + Nav3FragmentMode.REPLACE -> "Open Products Fragment" + Nav3FragmentMode.DIALOG -> "Open Checkout dialog" + Nav3FragmentMode.PAGER -> "Open ViewPager2" + }, + onClick = { onOpenFragments(mode) }, + testTag = + nav3TestTag( + if (mode == Nav3FragmentMode.REPLACE) "multi_fragment_open_fragments" + else "${mode.name.lowercase()}_open_fragments" + ), ) }, ) } private fun Fragment.nav3RootComposeView( - activity: ComponentActivity, + activity: Nav3FragmentsActivity, host: String, + title: String = host, initialRoute: Nav3Route, configuration: NavigationSampleConfig, onFinish: () -> Unit, @@ -338,9 +380,9 @@ private fun Fragment.nav3RootComposeView( CompositionLocalProvider(LocalNav3SampleHost provides host) { MultiRootNavDisplay( activity = activity, - title = host, + title = title, initialRoute = initialRoute, - scenario = Nav3Scenario.MULTI_FRAGMENT, + scenario = activity.mode.scenario, configuration = configuration, onFinish = onFinish, onCheckout = onCheckout, @@ -353,38 +395,29 @@ private fun Fragment.nav3RootComposeView( @OptIn(ExperimentalComposeUiApi::class) @Composable private fun MultiFragmentControls( + mode: Nav3FragmentMode, slowTransitions: Boolean, onSlowTransitionsChanged: (Boolean) -> Unit, - onOpenCheckoutSheet: () -> Unit, - onOpenPager: () -> Unit, ) { Surface( shadowElevation = 4.dp, modifier = Modifier.statusBarsPadding().semantics { testTagsAsResourceId = true }, ) { Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp)) { - Text("Fragments Activity", style = MaterialTheme.typography.titleMedium) - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - OutlinedButton( - onClick = onOpenCheckoutSheet, - modifier = Modifier.testTag(nav3TestTag("multi_fragment_open_checkout_sheet")), - ) { - Text("Checkout sheet") - } - OutlinedButton( - onClick = onOpenPager, - modifier = Modifier.testTag(nav3TestTag("multi_fragment_open_pager")), - ) { - Text("Pager") + Text(mode.scenario.label, style = MaterialTheme.typography.titleMedium) + when (mode) { + Nav3FragmentMode.REPLACE -> { + Row(verticalAlignment = Alignment.CenterVertically) { + Text("Slow Fragment transitions", modifier = Modifier.weight(1f)) + Switch( + checked = slowTransitions, + onCheckedChange = onSlowTransitionsChanged, + modifier = Modifier.testTag(nav3TestTag("multi_fragment_slow_transitions")), + ) + } } - } - Row(verticalAlignment = Alignment.CenterVertically) { - Text("Slow Fragment transitions", modifier = Modifier.weight(1f)) - Switch( - checked = slowTransitions, - onCheckedChange = onSlowTransitionsChanged, - modifier = Modifier.testTag(nav3TestTag("multi_fragment_slow_transitions")), - ) + Nav3FragmentMode.DIALOG, + Nav3FragmentMode.PAGER -> Unit } } } diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav3/Nav3RouteScreens.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav3/Nav3RouteScreens.kt index 314d427456..64e3ee41f3 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav3/Nav3RouteScreens.kt +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav3/Nav3RouteScreens.kt @@ -734,21 +734,6 @@ internal fun ShareSheetRoute( } } -@Composable -internal fun FutureRoute(routeName: String, scenario: String) { - RouteScaffold( - routeSpec = - RouteSpecs.get(routeName) - .copy( - title = "$routeName: WIP", - description = - "Reserved for a future milestone when SentryNavEffect supports $scenario navigation " + - "state.", - ), - testTagPrefix = nav3RouteTag(routeName), - ) -} - @OptIn(ExperimentalComposeUiApi::class) @Composable private fun Nav3ProductListItem(index: Int) { diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav3/Nav3Routes.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav3/Nav3Routes.kt index 554a5d3c3d..70b5232c53 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav3/Nav3Routes.kt +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav3/Nav3Routes.kt @@ -39,8 +39,25 @@ internal fun Nav3Route.routeSpec(): RouteSpec = routeName = Nav3Route.MultiFragment.routeName, title = "Multi-Fragment", description = - "Tests SentryNavEffect when each Fragment owns its own SentryNavEffect and NavDisplay: " + - "Fragment replace transitions, a dialog overlay, and a ViewPager2 of Fragments.", + "Replace a Products Fragment with a Checkout Fragment. Each Fragment owns its own " + + "SentryNavEffect and NavDisplay. Try a slow transition and press Back during it.", + ) + Nav3Route.FragmentOverlay -> + RouteSpec( + routeName = Nav3Route.FragmentOverlay.routeName, + title = "Fragment: Overlay", + description = + "Open a full-screen Checkout DialogFragment over a Products Fragment. Both own a " + + "SentryNavEffect and NavDisplay, and Products stays resumed underneath. Back dismisses " + + "the dialog and reveals Products.", + ) + Nav3Route.ViewPager2 -> + RouteSpec( + routeName = Nav3Route.ViewPager2.routeName, + title = "ViewPager2", + description = + "Each page is a Fragment with its own SentryNavEffect and NavDisplay. Neighboring " + + "pages are preloaded at STARTED. Back returns to the Products Fragment underneath.", ) Nav3Route.DeepLink -> RouteSpec( @@ -56,8 +73,6 @@ internal fun Nav3Route.routeSpec(): RouteSpec = is Nav3Route.Confirmation -> RouteSpecs.confirmation is Nav3Route.PromoDialog -> RouteSpecs.promoDialog is Nav3Route.ShareSheet -> RouteSpecs.shareSheet - Nav3Route.Multipane, - Nav3Route.Multistack, is Nav3Route.Performance -> RouteSpec(routeName = routeName, title = routeName) } @@ -87,6 +102,8 @@ private fun Nav3Route.toSavedState(): Bundle = Nav3Route.Custom -> putString("type", "custom") Nav3Route.MultiActivity -> putString("type", "multi_activity") Nav3Route.MultiFragment -> putString("type", "multi_fragment") + Nav3Route.FragmentOverlay -> putString("type", "fragment_overlay") + Nav3Route.ViewPager2 -> putString("type", "view_pager2") Nav3Route.DeepLink -> putString("type", "deep_link") Nav3Route.ProductList -> putString("type", "product_list") is Nav3Route.ProductDetail -> { @@ -111,8 +128,6 @@ private fun Nav3Route.toSavedState(): Bundle = putString("type", "share_sheet") putString("product_id", productId) } - Nav3Route.Multipane -> putString("type", "multipane") - Nav3Route.Multistack -> putString("type", "multistack") is Nav3Route.Performance -> { putString("type", "performance") putInt("index", index) @@ -127,7 +142,12 @@ private fun Bundle.toNav3Route(): Nav3Route { "single_stack" -> Nav3Route.SingleStack "custom" -> Nav3Route.Custom "multi_activity" -> Nav3Route.MultiActivity - "multi_fragment" -> Nav3Route.MultiFragment + "multi_fragment", + "fragment_replace" -> Nav3Route.MultiFragment + "dialog_overlay", + "fragment_overlay" -> Nav3Route.FragmentOverlay + "view_pager_pages", + "view_pager2" -> Nav3Route.ViewPager2 "deep_link" -> Nav3Route.DeepLink "product_list" -> Nav3Route.ProductList "product_detail" -> { @@ -147,8 +167,6 @@ private fun Bundle.toNav3Route(): Nav3Route { Nav3Route.PromoDialog(promoId = getString("promo_id") ?: return Nav3Route.SingleStack) "share_sheet" -> Nav3Route.ShareSheet(productId = getString("product_id") ?: return Nav3Route.SingleStack) - "multipane" -> Nav3Route.Multipane - "multistack" -> Nav3Route.Multistack "performance" -> Nav3Route.Performance( index = getInt("index"), @@ -167,12 +185,12 @@ internal fun SnapshotStateList.openScenario(scenario: Nav3Scenario) { when (scenario) { Nav3Scenario.LANDING -> resetTo(Nav3Route.Landing) Nav3Scenario.SINGLE_STACK -> resetTo(Nav3Route.SingleStack) - Nav3Scenario.CUSTOM -> resetTo(Nav3Route.Custom) Nav3Scenario.MULTI_ACTIVITY -> resetTo(Nav3Route.MultiActivity) Nav3Scenario.MULTI_FRAGMENT -> resetTo(Nav3Route.MultiFragment) + Nav3Scenario.FRAGMENT_OVERLAY -> resetTo(Nav3Route.FragmentOverlay) + Nav3Scenario.VIEW_PAGER2 -> resetTo(Nav3Route.ViewPager2) Nav3Scenario.DEEP_LINK -> resetTo(Nav3Route.DeepLink) - Nav3Scenario.MULTIPANE -> resetTo(Nav3Route.Multipane) - Nav3Scenario.MULTIPLE_STACKS -> resetTo(Nav3Route.Multistack) + Nav3Scenario.CUSTOM -> resetTo(Nav3Route.Custom) Nav3Scenario.PERFORMANCE -> resetTo(Nav3Route.Performance(index = 0, generation = 0)) } } @@ -241,6 +259,14 @@ internal sealed interface Nav3Route { override val routeName: String = "MultiFragment" } + data object FragmentOverlay : Nav3Route { + override val routeName: String = "FragmentOverlay" + } + + data object ViewPager2 : Nav3Route { + override val routeName: String = "ViewPager2" + } + data object DeepLink : Nav3Route { override val routeName: String = RouteNames.DEEP_LINK } @@ -289,16 +315,6 @@ internal sealed interface Nav3Route { override val previewName: String = "ShareSheet($productId)" } - data object Multipane : Nav3Route { - override val routeName: String = "Multipane" - override val arguments: Map = mapOf("scenario" to "multipane") - } - - data object Multistack : Nav3Route { - override val routeName: String = "Multistack" - override val arguments: Map = mapOf("scenario" to "multistack") - } - data class Performance(val index: Int, val generation: Int) : Nav3Route { override val routeName: String = "Performance" override val arguments: Map = mapOf("index" to index, "generation" to generation) @@ -311,11 +327,11 @@ internal enum class Nav3Scenario(val label: String, val showTab: Boolean = true) LANDING(RouteNames.LANDING, showTab = false), SINGLE_STACK("Single Stack"), DEEP_LINK("Deep Link"), - CUSTOM("Custom"), MULTI_ACTIVITY("Multi-Activity"), MULTI_FRAGMENT("Multi-Fragment"), - MULTIPANE("Multipane"), - MULTIPLE_STACKS("Multistack"), + FRAGMENT_OVERLAY("Fragment: Overlay"), + VIEW_PAGER2("ViewPager2"), + CUSTOM("Custom"), PERFORMANCE("Performance"), } @@ -324,11 +340,11 @@ internal val Nav3Scenario.initialRoute: Nav3Route when (this) { Nav3Scenario.LANDING -> Nav3Route.Landing Nav3Scenario.SINGLE_STACK -> Nav3Route.SingleStack - Nav3Scenario.CUSTOM -> Nav3Route.Custom Nav3Scenario.MULTI_ACTIVITY -> Nav3Route.MultiActivity Nav3Scenario.MULTI_FRAGMENT -> Nav3Route.MultiFragment + Nav3Scenario.FRAGMENT_OVERLAY -> Nav3Route.FragmentOverlay + Nav3Scenario.VIEW_PAGER2 -> Nav3Route.ViewPager2 Nav3Scenario.DEEP_LINK -> Nav3Route.DeepLink - Nav3Scenario.MULTIPANE -> Nav3Route.Multipane - Nav3Scenario.MULTIPLE_STACKS -> Nav3Route.Multistack + Nav3Scenario.CUSTOM -> Nav3Route.Custom Nav3Scenario.PERFORMANCE -> Nav3Route.Performance(index = 0, generation = 0) }