Skip to content

Instantly share code, notes, and snippets.

@nikclayton
Last active February 17, 2026 11:25
Show Gist options
  • Select an option

  • Save nikclayton/dbaabf6df5e0b593f2a66f74230a2497 to your computer and use it in GitHub Desktop.

Select an option

Save nikclayton/dbaabf6df5e0b593f2a66f74230a2497 to your computer and use it in GitHub Desktop.

I have a list of items, each item has a "Delete" button. Pressing the button should show a dialog asking the user to confirm the deletion, and include the name of the item being deleted.

Currently, I have this, which works:

In the viewmodel

// The dialog's state. Either hidden or visible. If visible it needs to display
// text specific to the item associated with the button the user pressed.
sealed interface ConfirmDeleteItemDialogState {
    data object Hidden : ConfirmDeleteItemDialogState
    data class Visible(val item: Item): ConfirmDeleteItemDialogState
}

// ...

class MyViewModel(/* ... */) : ViewModel() {
    // ...
    
    val confirmDeleteItemDialogState: StateFlow<ConfirmDeleteItemDialogState>
        field = MutableStateFlow<ConfirmDeleteItemDialogState(ConfirmDeleteItemDialogState.Hidden)
        
    // ...
    
    // Called when the user clicks the button in a list entry to delete an item.
    fun showConfirmDeleteItemDialog(item: item) {
        confirmDeleteItemDialogState.update {
            ConfirmDeleteItemDialogState.Visible(item)
        }
    }
    
    fun hideConfirmDeleteItemDialog() {
        confirmDeleteItemDialogState.update {
            ConfirmDeleteItemDialogState.Hidden
        }
    }
    
    // Called when the user clicks the "OK" button in the confirmation dialog.
    fun deleteItem(item: Item) {
        hideConfirmDeleteDialog()
        // ... code that does the deletion here
    }
    
    // ...
}

The Screen Composable

@Composable
fun ItemListScreen(
    viewModel: MyViewModel
) {
    // ...
    
    val confirmDeleteItemDialogState by viewModel.confirmDeleteItemDialogState
        .collectAsStateWithLifecycle(ConfirmDeleteItemDialogState.Hidden)
        
    // ...
    
    AppTheme {
        // ...
        
        // Show the "Delete item X?" dialog. The dialog's title needs to include the
        // value of the `name` property from the given `item`.
        if (confirmDeleteItemDialogState is ConfirmDeleteItemDialogState.Visible) {
            val item = confirmDeleteItemDialogState.item
            
            MyAlertDialog(
                onDismissRequest = { viewModel.hideConfirmDeleteItemDialog() },
                onConfirmation = { viewModel.deleteItem(item) },
                dialogTitle = stringResource(R.id.confirm_delete_item, item.name)
                // ...
            )
        )
    }
}

The dialog

Note how the state parameter is assigned a constant and never changed, because the presence of the dialog is determined by the if statement in ItemListScreen.

@Composable
private fun MyAlertDialog(
    onDismissRequest: () -> Unit,
    onConfirmation: () -> Unit,
    dialogTitle: String,
    dialogText: String,
) {
    UnstyledScrim()
    UnstyledDialog(
        state = rememberDialogState(initiallyVisible = true),
        onDismiss = onDismissRequest,
    ) {
        // ...
    }
}
@nikclayton
Copy link
Author

nikclayton commented Feb 16, 2026

Alternate.

The Screen Composable

@Composable
fun ItemListScreen(
    viewModel: MyViewModel
) {
    // ...
   
    val confirmDeleteItemDialogVisibility = rememberDialogState()
 
    val confirmDeleteItemDialogState by viewModel.confirmDeleteItemDialogState
        .collectAsStateWithLifecycle(ConfirmDeleteItemDialogState.Hidden)
        
    // ...
    
    AppTheme {
        // ...
        
        // Show the "Delete item X?" dialog. The dialog's title needs to include the
        // value of the `name` property from the given `item`.
        confirmDeleteItemDialogVisibility.visible = confirmDeleteItemDialogState is ConfirmDeleteItemDialogState.Visible
        val item = (confirmDeleteItemDialogState as? ConfirmDeleteItemDialogState.Visible)?.item

        MyAlertDialog(
            state = confirmDeleteItemVisibility
            onDismissRequest = { viewModel.hideConfirmDeleteItemDialog() },
            onConfirmation = { item?.let { viewModel.deleteItem(item) } },
            dialogTitle = stringResource(R.id.confirm_delete_item, item?.name ?: "")
            // ...
        )
    )
}

The dialog

@Composable
private fun MyAlertDialog(
    state: DialogState,
    onDismissRequest: () -> Unit,
    onConfirmation: () -> Unit,
    dialogTitle: String,
    dialogText: String,
) {
    UnstyledScrim()
    UnstyledDialog(
        state = state,
        onDismiss = onDismissRequest,
    ) {
        // ...
    }
}

@nikclayton
Copy link
Author

nikclayton commented Feb 16, 2026

Or

The Screen Composable

@Composable
fun ItemListScreen(
    viewModel: MyViewModel
) {
    // ...
   
    val confirmDeleteItemDialogState by viewModel.confirmDeleteItemDialogState
        .collectAsStateWithLifecycle(ConfirmDeleteItemDialogState.Hidden)
        
    // ...
    
    AppTheme {
        // ...
        
        // Show the "Delete item X?" dialog. The dialog's title needs to include the
        // value of the `name` property from the given `item`.
        val item = (confirmDeleteItemDialogState as? ConfirmDeleteItemDialogState.Visible)?.item

        MyAlertDialog(
            isVisible = confirmDeleteItemDialogState is ConfirmDeleteItemDialogState.Visible,
            onDismissRequest = { viewModel.hideConfirmDeleteItemDialog() },
            onConfirmation = { item?.let { viewModel.deleteItem(it) } },
            dialogTitle = stringResource(R.id.confirm_delete_item, item?.name ?: "")
            // ...
        )
    )
}

The dialog

@Composable
private fun MyAlertDialog(
    isVisible: Boolean = false,
    onDismissRequest: () -> Unit,
    onConfirmation: () -> Unit,
    dialogTitle: String,
    dialogText: String,
) {
    val dialogState = rememberDialogState(initiallyVisible = false)
    SideEffect { dialogState.visible = isVisible }

    UnstyledScrim()
    UnstyledDialog(
        state = dialogState,
        onDismiss = onDismissRequest,
    ) {
        // ...
    }
}

This approach has a problem; if you try and preview it, Android Studio shows nothing:

@Preview
@Composable
private fun PreviewMyAlertDialog() {
    AppTheme {
        MyAlertDialog(
            visible = true,
            onDismissRequest = {},
            onConfirmation = {},
            dialogTitle = "An alert dialog",
            dialogText = "The alert dialog message",
        )
    }
}

You can "fix" this by changing the declaration of MyAlertDialog.dialogState to val dialogState = rememberDialogState(initiallyVisible = true) (i.e., false -> true), but then the dialog briefly flashes on screen when parent composable first appears. This can be fixed / worked around by changing the declaration of dialogState to val dialogState = rememberDialogState(initiallyVisible = LocalInspectionMode.current)

@nikclayton
Copy link
Author

nikclayton commented Feb 17, 2026

Other problem is the dynamic text in the dialog can disappear when the dialog is dismissed.

Possible solution here -- remember the text to use, and update it whenever the dialog visibility transitions from invisible to visible.

Lines marked "// XXX" indicate changes:

@Composable
private fun PachliAlertDialog(
    isVisible: Boolean = false,
    onDismissRequest: () -> Unit,
    onConfirmation: () -> Unit,
    dialogTitle: String,
    dialogText: String,
) {
    val dialogState = rememberDialogState(initiallyVisible = LocalInspectionMode.current)
    SideEffect { dialogState.visible = isVisible }

    /** The actual title to display. */
    var title by remember { mutableStateOf(dialogTitle) }  // XXX

    /** The actual text to display. */
    var text by remember { mutableStateOf(dialogText) }  // XXX

    // XXX
    // If the dialog is becoming visible then save the title and text to display.
    // These will be used while the dialog is hidden, to ensure the content does
    // not disappear when the dialog is dismissed.
    if (isVisible && !dialogState.visible) {
        title = dialogTitle
        text = dialogText
    }

    UnstyledDialog(
        state = dialogState,
        onDismiss = onDismissRequest,
    ) {
        UnstyledScrim()
        UnstyledDialogPanel(
            modifier = Modifier
                .displayCutoutPadding()
                .systemBarsPadding()
                .widthIn(min = 280.dp, max = 560.dp)
                .padding(start = 24.dp, top = 18.dp, end = 24.dp, bottom = 0.dp)
                .clip(RoundedCornerShape(12.dp))
                .background(Color.White),
            enter = fadeIn(),
            exit = fadeOut(),
        ) {
            Column {
                Column(Modifier.padding(start = 24.dp, top = 24.dp, end = 24.dp)) {
                    Text(
                        text = title,  // XXX
                        style = TextStyle(fontWeight = FontWeight.Medium),
                        fontSize = 20.sp,
                    )
                    Spacer(Modifier.height(8.dp))
                    Text(
                        modifier = Modifier.defaultMinSize(minHeight = 48.dp),
                        text = text,  // XXX
                        fontSize = 16.sp,
                    )
                Row(
                    modifier = Modifier
                        .align(Alignment.End)
                        .padding(start = 12.dp, top = 4.dp, end = 12.dp, bottom = 4.dp),
                ) {
                  // Cancel and OK buttons go here
                }
            }
        }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment