Skip to content

Commit dbc7ceb

Browse files
authored
chore(android): Add product list to Nav2 sample (#6060)
Add a new hide/show product list button to the Nav2Activity. For the Compose tab, doing so lets us observe the span-producing of multiple SentryTraced composables inside the same nav transaction. (The list was also added to the Fragments tab to maintain UI parity.)
1 parent a9e53be commit dbc7ceb

2 files changed

Lines changed: 201 additions & 12 deletions

File tree

‎sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2ComposeRoutes.kt‎

Lines changed: 75 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@ import androidx.compose.material3.Text
2727
import androidx.compose.material3.TextButton
2828
import androidx.compose.runtime.Composable
2929
import androidx.compose.runtime.LaunchedEffect
30+
import androidx.compose.runtime.getValue
3031
import androidx.compose.runtime.mutableStateListOf
3132
import androidx.compose.runtime.mutableStateOf
3233
import androidx.compose.runtime.rememberUpdatedState
3334
import androidx.compose.runtime.saveable.listSaver
3435
import androidx.compose.runtime.saveable.rememberSaveable
36+
import androidx.compose.runtime.setValue
3537
import androidx.compose.runtime.snapshots.SnapshotStateList
3638
import androidx.compose.ui.ExperimentalComposeUiApi
3739
import androidx.compose.ui.Modifier
@@ -313,16 +315,47 @@ private fun Nav2ComposeHomeRoute(routeSpec: Nav2RouteSpec, onBrowseProducts: ()
313315
Nav2ComposeActionRoute(routeSpec, buttons = listOf("Browse Products" to onBrowseProducts))
314316
}
315317

318+
@OptIn(ExperimentalComposeUiApi::class)
316319
@Composable
317320
private fun Nav2ComposeProductListRoute(
318321
routeSpec: Nav2RouteSpec,
319322
onOpenProduct42: () -> Unit,
320323
onOpenProduct7: () -> Unit,
321324
) {
322-
Nav2ComposeActionRoute(
323-
routeSpec,
324-
buttons = listOf("Open Product 42" to onOpenProduct42, "Open Product 7" to onOpenProduct7),
325-
)
325+
var showProductItems by rememberSaveable { mutableStateOf(false) }
326+
327+
Nav2ComposeRouteScaffold(
328+
routeSpec = routeSpec,
329+
cardContent = {
330+
SentryTraced(
331+
tag = "product_list_actions",
332+
modifier = Modifier.fillMaxWidth(),
333+
enableUserInteractionTracing = false,
334+
) {
335+
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
336+
Nav2ComposeRouteButton("Open Product 42", onOpenProduct42)
337+
Nav2ComposeRouteButton("Open Product 7", onOpenProduct7)
338+
}
339+
}
340+
},
341+
) {
342+
Nav2ComposeRouteButton(
343+
label = if (showProductItems) "Hide Product Items" else "Show Product Items",
344+
onClick = { showProductItems = !showProductItems },
345+
)
346+
347+
if (showProductItems) {
348+
SentryTraced(
349+
tag = "product_list_items",
350+
modifier = Modifier.fillMaxWidth(),
351+
enableUserInteractionTracing = false,
352+
) {
353+
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
354+
repeat(PRODUCT_LIST_ITEM_COUNT) { index -> Nav2ComposeProductListItem(index + 1) }
355+
}
356+
}
357+
}
358+
}
326359
}
327360

328361
@Composable
@@ -388,12 +421,15 @@ private fun Nav2ComposeActionRoute(
388421
arguments: Map<String, Any?> = emptyMap(),
389422
buttons: List<Pair<String, () -> Unit>>,
390423
) {
391-
Nav2ComposeRouteScaffold(routeSpec) {
392-
routeSpec.displayArguments(arguments).forEach { (label, value) ->
393-
Nav2ComposeRouteInfo(label, value)
394-
}
395-
buttons.forEach { (label, onClick) -> Nav2ComposeRouteButton(label, onClick) }
396-
}
424+
Nav2ComposeRouteScaffold(
425+
routeSpec = routeSpec,
426+
cardContent = {
427+
routeSpec.displayArguments(arguments).forEach { (label, value) ->
428+
Nav2ComposeRouteInfo(label, value)
429+
}
430+
buttons.forEach { (label, onClick) -> Nav2ComposeRouteButton(label, onClick) }
431+
},
432+
)
397433
}
398434

399435
@Composable
@@ -489,6 +525,7 @@ private fun Nav2ComposeShareSheetRoute(
489525
@Composable
490526
private fun Nav2ComposeRouteScaffold(
491527
routeSpec: Nav2RouteSpec,
528+
cardContent: (@Composable ColumnScope.() -> Unit)? = null,
492529
content: (@Composable ColumnScope.() -> Unit)? = null,
493530
) {
494531
Column(
@@ -501,7 +538,7 @@ private fun Nav2ComposeRouteScaffold(
501538
fontWeight = FontWeight.Bold,
502539
)
503540
routeSpec.description?.let { Text(it, style = MaterialTheme.typography.bodyMedium) }
504-
if (content != null) {
541+
if (cardContent != null) {
505542
Card(
506543
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
507544
modifier = Modifier.fillMaxWidth(),
@@ -510,10 +547,34 @@ private fun Nav2ComposeRouteScaffold(
510547
modifier = Modifier.padding(16.dp),
511548
verticalArrangement = Arrangement.spacedBy(8.dp),
512549
) {
513-
content()
550+
cardContent()
514551
}
515552
}
516553
}
554+
content?.invoke(this)
555+
}
556+
}
557+
558+
@OptIn(ExperimentalComposeUiApi::class)
559+
@Composable
560+
private fun Nav2ComposeProductListItem(index: Int) {
561+
SentryTraced(
562+
tag = "product_list_item_$index",
563+
modifier = Modifier.fillMaxWidth(),
564+
enableUserInteractionTracing = false,
565+
) {
566+
Card(
567+
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
568+
modifier = Modifier.fillMaxWidth(),
569+
) {
570+
Row(
571+
modifier = Modifier.fillMaxWidth().padding(12.dp),
572+
horizontalArrangement = Arrangement.SpaceBetween,
573+
) {
574+
Text("Product #$index", fontWeight = FontWeight.Bold)
575+
Text("SKU-$index")
576+
}
577+
}
517578
}
518579
}
519580

@@ -534,6 +595,8 @@ private fun Nav2ComposeRouteButton(
534595

535596
private fun nav2ComposeInteractionTag(label: String): String = "Nav2 Compose $label"
536597

598+
private const val PRODUCT_LIST_ITEM_COUNT = 20
599+
537600
@Composable
538601
private fun Nav2ComposeRouteInfo(label: String, value: String) {
539602
Row(

‎sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2RouteFragment.kt‎

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@ import android.view.View
77
import android.view.ViewGroup
88
import android.widget.Button
99
import android.widget.LinearLayout
10+
import android.widget.ScrollView
1011
import android.widget.TextView
12+
import androidx.core.view.doOnPreDraw
1113
import androidx.core.view.setPadding
1214
import androidx.fragment.app.Fragment
1315
import androidx.lifecycle.lifecycleScope
16+
import io.sentry.ISpan
17+
import io.sentry.Sentry
1418
import io.sentry.samples.android.R
1519
import kotlinx.coroutines.launch
1620

@@ -64,6 +68,7 @@ class Nav2RouteFragment : Fragment() {
6468
activity.navigateTo(Nav2Destination.ProductDetail("7", "product-list"))
6569
},
6670
),
71+
trailingContent = { addProductListItemsToggle() },
6772
)
6873

6974
Nav2RouteNames.PRODUCT_DETAIL -> productDetailLayout(activity)
@@ -163,6 +168,7 @@ class Nav2RouteFragment : Fragment() {
163168
routeSpec: Nav2RouteSpec,
164169
arguments: Bundle? = null,
165170
buttons: List<RouteButton> = emptyList(),
171+
trailingContent: (LinearLayout.() -> Unit)? = null,
166172
): View {
167173
val info = routeSpec.displayArguments(arguments)
168174
return LinearLayout(requireContext()).apply {
@@ -182,9 +188,121 @@ class Nav2RouteFragment : Fragment() {
182188
}
183189
)
184190
}
191+
trailingContent?.invoke(this)
185192
}
186193
}
187194

195+
private fun LinearLayout.addProductListItemsToggle() {
196+
var itemsCreated = false
197+
var itemsVisible = false
198+
val listContainer =
199+
LinearLayout(context).apply {
200+
orientation = LinearLayout.VERTICAL
201+
setPadding(0, 8.dp, 0, 0)
202+
}
203+
val scrollView =
204+
ScrollView(context).apply {
205+
visibility = View.GONE
206+
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, 280.dp)
207+
addView(listContainer, ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT))
208+
}
209+
210+
addView(
211+
Button(context).apply {
212+
text = "Show Product Items"
213+
isAllCaps = false
214+
setOnClickListener {
215+
itemsVisible = !itemsVisible
216+
text = if (itemsVisible) "Hide Product Items" else "Show Product Items"
217+
if (itemsVisible && !itemsCreated) {
218+
populateProductListItems(listContainer)
219+
itemsCreated = true
220+
}
221+
scrollView.visibility = if (itemsVisible) View.VISIBLE else View.GONE
222+
}
223+
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
224+
}
225+
)
226+
addView(scrollView)
227+
}
228+
229+
private fun populateProductListItems(listContainer: LinearLayout) {
230+
val ownerSpan = Sentry.getSpan()
231+
val compositionParent =
232+
ownerSpan?.startChild(
233+
OP_PARENT_COMPOSITION,
234+
"Fragment Product List Item List Composition",
235+
)
236+
try {
237+
recordManualUiSpan(compositionParent, OP_COMPOSE, "fragment_product_list_items") {
238+
repeat(PRODUCT_LIST_ITEM_COUNT) { index ->
239+
val itemNumber = index + 1
240+
recordManualUiSpan(
241+
compositionParent,
242+
OP_COMPOSE,
243+
"fragment_product_list_item_$itemNumber",
244+
) {
245+
listContainer.addView(productListItemRow(itemNumber))
246+
}
247+
}
248+
}
249+
} finally {
250+
compositionParent?.finish()
251+
}
252+
253+
listContainer.doOnPreDraw {
254+
val renderParent =
255+
ownerSpan?.startChild(
256+
OP_PARENT_RENDER,
257+
"Fragment Product List Item List Render",
258+
)
259+
try {
260+
recordManualUiSpan(renderParent, OP_RENDER, "fragment_product_list_items")
261+
repeat(PRODUCT_LIST_ITEM_COUNT) { index ->
262+
recordManualUiSpan(renderParent, OP_RENDER, "fragment_product_list_item_${index + 1}")
263+
}
264+
} finally {
265+
renderParent?.finish()
266+
}
267+
}
268+
}
269+
270+
private fun recordManualUiSpan(
271+
parentSpan: ISpan?,
272+
operation: String,
273+
description: String,
274+
block: () -> Unit = {},
275+
) {
276+
val span = parentSpan?.startChild(operation, description)
277+
span?.setData("sample.nav2_fragment_manual_ui_span", true)
278+
try {
279+
block()
280+
} finally {
281+
span?.finish()
282+
}
283+
}
284+
285+
private fun productListItemRow(itemNumber: Int): View =
286+
LinearLayout(requireContext()).apply {
287+
orientation = LinearLayout.HORIZONTAL
288+
setPadding(12.dp)
289+
setBackgroundColor(color(android.R.color.white))
290+
addView(
291+
TextView(context).apply {
292+
text = "Product #$itemNumber"
293+
textSize = 14f
294+
setTypeface(null, Typeface.BOLD)
295+
layoutParams = LinearLayout.LayoutParams(0, WRAP_CONTENT, 1f)
296+
}
297+
)
298+
addView(
299+
TextView(context).apply {
300+
text = "SKU-$itemNumber"
301+
textSize = 14f
302+
}
303+
)
304+
}
305+
188306
private fun titleText(textValue: String): TextView =
189307
TextView(requireContext()).apply {
190308
text = textValue
@@ -224,4 +342,12 @@ class Nav2RouteFragment : Fragment() {
224342
get() = (this * resources.displayMetrics.density).toInt()
225343

226344
private fun color(id: Int): Int = requireContext().getColor(id)
345+
346+
private companion object {
347+
private const val PRODUCT_LIST_ITEM_COUNT = 20
348+
private const val OP_PARENT_COMPOSITION = "ui.compose.composition"
349+
private const val OP_COMPOSE = "ui.compose"
350+
private const val OP_PARENT_RENDER = "ui.compose.rendering"
351+
private const val OP_RENDER = "ui.render"
352+
}
227353
}

0 commit comments

Comments
 (0)