androidandroid-jetpack-composebottom-sheetmodal-sheet

Where to open modal Bottom Sheet in Jetpack Compose?


In my Android app, I am using Google Maps and displaying markers on the map. When a user clicks on one of the markers, I want to open a modal Bottom Sheet to display marker details. Where should I implement sheet code, in top most composable or in marker composable which displays a marker?

What is the best way to open the Modal Sheet quickly?

here is what I am currently doing:

    @OptIn(ExperimentalMaterial3Api::class)
@Composable
fun A() {
    // Sheet state should be handled at the highest relevant level
    val sheetState = rememberModalBottomSheetState(
        skipPartiallyExpanded = false
    )
    var showBottomSheet by remember { mutableStateOf(false) }
    var selectedMarker by remember { mutableStateOf<MarkerData?>(null) }
    
    // Handler to be passed down
    val onMarkerClick: (MarkerData) -> Unit = { markerData ->
        selectedMarker = markerData
        showBottomSheet = true
    }

    // Main content
    Box(modifier = Modifier.fillMaxSize()) {
        Maps(onMarkerClick = onMarkerClick)
        
        // BottomSheet
        if (showBottomSheet) {
            ModalBottomSheet(
                onDismissRequest = { 
                    showBottomSheet = false
                    selectedMarker = null 
                },
                sheetState = sheetState
            ) {
                // Sheet content
                MarkerDetailContent(
                    markerData = selectedMarker,
                    onDismiss = { 
                        showBottomSheet = false
                        selectedMarker = null 
                    }
                )
            }
        }
    }
}

@Composable
fun Maps(
    onMarkerClick: (MarkerData) -> Unit
) {
    // Map implementation
    GoogleMap(
        modifier = Modifier.fillMaxSize(),
        properties = MapProperties(/*...*/)
    ) {
        Markers(onMarkerClick = onMarkerClick)
    }
}

@Composable
fun Markers(
    onMarkerClick: (MarkerData) -> Unit
) {
    // Marker data (could come from ViewModel)
    val markers = remember { /* Your markers data */ }
    
    markers.forEach { markerData ->
        Marker(
            state = rememberMarkerState(
                position = markerData.position
            ),
            onClick = {
                onMarkerClick(markerData)
                true // consume the event
            }
        )
    }
}

@Composable
fun MarkerDetailContent(
    markerData: MarkerData?,
    onDismiss: () -> Unit
) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp)
    ) {
        Text(
            text = "Marker Details",
            style = MaterialTheme.typography.headlineSmall,
            modifier = Modifier.padding(bottom = 16.dp)
        )
        
        markerData?.let {
            Text("Title: ${it.title}")
            Text("Position: ${it.position}")
            // Add more marker details
        }
        
        Spacer(modifier = Modifier.height(16.dp))
        
        Button(
            onClick = onDismiss,
            modifier = Modifier.fillMaxWidth()
        ) {
            Text("Close")
        }
    }
}

// Data class for markers
data class MarkerData(
    val id: String,
    val title: String,
    val position: LatLng,
    // Add other marker properties
)

Solution

  • It technically doesn't matter where you call it, as it will always display above the whole screen. However, I would say that how you implemented it is a good practice. As the ModalBottomSheet semantically impacts the whole screen, I would also rather place it in a top-level Composable.

    Note that how you currently implemented the ModalBottomSheet, it will immediately be hidden without any animation when you press the Button with label "Close". Instead, first hide() it and then update the showBottomSheet variable accordingly:

    val scope = rememberCoroutineScope()  // on top of Composable, add this line
    
    //...
    
    MarkerDetailContent(
        markerData = selectedMarker,
        onDismiss = { 
            scope.launch {
                sheetState.hide()  // hide BottomSheet with animation
            }.invokeOnCompletion {
                if (!bottomSheetState.isVisible) {
                    showBottomSheet = false  // update variable afterwards
                    selectedMarker = null 
                }
            }
        }
    )