From 1e006b7d29bd12bca4b620231bc205d159a7fc68 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Fri, 23 Oct 2026 15:49:45 +0200 Subject: [PATCH] ref(android): Split Nav3 fragment scenarios into tabs in the Nav3 sample Give fragment replacement, dialog overlays, and ViewPager2 pages separate entry points while preserving their test paths. Remove unused placeholders and place Custom before Performance. Co-Authored-By: Codex --- .../android/navigation/nav3/Nav3Activity.kt | 41 ++++--- .../navigation/nav3/Nav3MultiFragment.kt | 113 +++++++++++------- .../navigation/nav3/Nav3RouteScreens.kt | 15 --- .../android/navigation/nav3/Nav3Routes.kt | 72 ++++++----- 4 files changed, 140 insertions(+), 101 deletions(-) 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) }