How to create a Custom Dialog in Jetpack Compose for Kotlin Multiplatform

Gradient Warning Dialog KMP

Create a responsive warning dialog UI in Jetpack Compose, compatible with Kotlin Multiplatform (KMP), featuring animations, adaptive layout for orientations, and confirm/cancel functionality.

Features

Learn to implement a customizable responsive warning dialog UI with fade-in and scale animations, gradient styling, and orientation-aware design, optimized for KMP projects.

Key features of the Gradient Warning Dialog UI include:
  • Responsive Layout: Automatically adapts to portrait, landscape, and desktop using BoxWithConstraints.
  • Animations: Smooth fade-in and scale-in effects for engaging user experience.
  • Customizable: Parameters for title, message, and button actions.
  • KMP Compatible: Utilizes Compose Multiplatform for cross-platform support.

Step-by-Step Implementation

Follow these steps to create a Gradient Warning Dialog UI in your KMP project:

  1. Define the Composable: Create GradientWarningDialog with parameters for title, message, dismiss, and confirm callbacks.
  2. Animate Fade and Scale: Use animateFloatAsState for alpha and scale animations.
  3. Build Main Layout: Use BoxWithConstraints to detect orientation and apply animations.
  4. Icon Section: Display a warning icon with scaling for desktop.
  5. Text Section: Add title and message with responsive font sizes.
  6. Button Section: Implement adaptive button layout for small and large screens.
  7. Define Constants: Create object for styling and animation durations.
  8. Demo Composable: Create a demo screen to showcase the dialog.

Step 1: Define the GradientWarningDialog Composable

Start by defining the composable with customizable parameters.

            
@Composable
fun GradientWarningDialog(
    title: String = "Warning",
    message: String = "This action cannot be undone. Proceed with caution!",
    onDismiss: () -> Unit,
    onConfirm: () -> Unit = {},
    modifier: Modifier = Modifier
) {
    val (alphaAnim, scaleAnim) = animateFadeAndScaleDialog()
    Dialog(
        onDismissRequest = onDismiss,
        properties = DialogProperties(dismissOnClickOutside = true)
    ) {
        BoxWithConstraints(
            modifier = modifier
                .fillMaxWidth()
                .wrapContentHeight()
                .padding(DialogConstants.PADDING)
        ) {
            // Further implementation
        }
    }
}
            
            
        

Step 2: Animate Fade and Scale

Use remembered animation states for fade-in and scale-in effects.

            
@Composable
private fun animateFadeAndScaleDialog(): Pair {
    val alpha by animateFloatAsState(
        targetValue = 1f,
        animationSpec = tween(
            durationMillis = DialogConstants.ALPHA_DURATION,
            easing = FastOutSlowInEasing
        ),
        label = "alpha"
    )
    val scale by animateFloatAsState(
        targetValue = 1f,
        animationSpec = tween(
            durationMillis = DialogConstants.SCALE_DURATION,
            easing = FastOutSlowInEasing
        ),
        label = "scale"
    )
    return alpha to scale
}
            
            
        

Step 3: Build Main Layout

Use BoxWithConstraints for orientation detection and apply gradient background.

            
BoxWithConstraints(
    modifier = modifier
        .fillMaxWidth()
        .wrapContentHeight()
        .padding(DialogConstants.PADDING)
) {
    val isLandscape = maxWidth > maxHeight
    val isDesktop = maxWidth > 800.dp
    val isSmallScreen = maxWidth < 400.dp
    val dialogWidth = when {
        isDesktop -> maxWidth * 0.55f
        isLandscape -> maxWidth * 0.75f
        else -> maxWidth * 0.9f
    }
    val maxDialogHeight = if (isDesktop) 500.dp else 400.dp
    Card(
        modifier = Modifier
            .width(dialogWidth)
            .wrapContentHeight()
            .heightIn(max = maxDialogHeight)
            .alpha(alphaAnim)
            .scale(scaleAnim),
        shape = RoundedCornerShape(DialogConstants.CORNER_RADIUS),
        colors = CardDefaults.cardColors(containerColor = Color.Transparent),
        elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
    ) {
        Column(
            modifier = Modifier
                .background(
                    Brush.linearGradient(
                        colors = listOf(
                            Color(0xFFFF8A65),
                            Color(0xFFF06292)
                        )
                    )
                )
                .padding(DialogConstants.CONTENT_PADDING),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.SpaceBetween
        ) {
            // Further implementation
        }
    }
}
            
            
        

Step 4: Icon Section

Display a warning icon with desktop scaling and animation.

            
@Composable
private fun WarningIconSection(isDesktop: Boolean) {
    val iconSize = if (isDesktop) 96.dp else DialogConstants.ICON_SIZE
    val iconPadding = if (isDesktop) 16.dp else DialogConstants.ICON_PADDING
    val iconScale by animateFloatAsState(
        targetValue = 1f,
        animationSpec = tween(
            durationMillis = DialogConstants.ICON_SCALE_DURATION,
            easing = FastOutSlowInEasing
        ),
        label = "iconScale"
    )
    Icon(
        imageVector = Icons.Rounded.Warning,
        contentDescription = "Warning",
        tint = Color.White,
        modifier = Modifier
            .size(iconSize)
            .scale(if (isDesktop) iconScale else 1f)
            .padding(bottom = iconPadding)
    )
}
            
            
        

Step 5: Text Section

Add title and message with responsive font sizes.

            
@Composable
private fun TextSection(title: String, message: String, isDesktop: Boolean) {
    val titleFontSize = if (isDesktop) DialogConstants.TITLE_FONT_SIZE * 1.2f else DialogConstants.TITLE_FONT_SIZE
    val messageFontSize = if (isDesktop) DialogConstants.MESSAGE_FONT_SIZE * 1.2f else DialogConstants.MESSAGE_FONT_SIZE
    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier.fillMaxWidth()
    ) {
        Text(
            text = title,
            color = Color.White,
            fontSize = titleFontSize,
            fontWeight = FontWeight.Bold,
            textAlign = TextAlign.Center,
            modifier = Modifier.padding(horizontal = DialogConstants.CONTENT_PADDING)
        )
        Spacer(modifier = Modifier.height(DialogConstants.SPACING))
        Text(
            text = message,
            color = Color.White.copy(alpha = 0.95f),
            fontSize = messageFontSize,
            textAlign = TextAlign.Center,
            lineHeight = messageFontSize * 1.4f,
            modifier = Modifier.padding(horizontal = DialogConstants.CONTENT_PADDING)
        )
    }
}
            
            
        

Step 6: Button Section

Implement adaptive button layout for small and large screens.

            
@Composable
private fun ButtonSection(onDismiss: () -> Unit, onConfirm: () -> Unit, isSmallScreen: Boolean) {
    if (isSmallScreen) {
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .padding(top = DialogConstants.SPACING),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            DialogButton(
                text = "Cancel",
                onClick = onDismiss,
                textColor = Color(0xFFF06292),
                modifier = Modifier
                    .fillMaxWidth(0.85f)
                    .height(DialogConstants.BUTTON_HEIGHT)
                    .padding(bottom = DialogConstants.BUTTON_PADDING)
            )
            DialogButton(
                text = "Proceed",
                onClick = {
                    onConfirm()
                    onDismiss()
                },
                textColor = Color(0xFFFF8A65),
                modifier = Modifier
                    .fillMaxWidth(0.85f)
                    .height(DialogConstants.BUTTON_HEIGHT)
            )
        }
    } else {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(top = DialogConstants.SPACING),
            horizontalArrangement = Arrangement.SpaceEvenly
        ) {
            DialogButton(
                text = "Cancel",
                onClick = onDismiss,
                textColor = Color(0xFFF06292),
                modifier = Modifier
                    .weight(1f)
                    .height(DialogConstants.BUTTON_HEIGHT)
                    .padding(horizontal = DialogConstants.BUTTON_PADDING)
            )
            DialogButton(
                text = "Proceed",
                onClick = {
                    onConfirm()
                    onDismiss()
                },
                textColor = Color(0xFFFF8A65),
                modifier = Modifier
                    .weight(1f)
                    .height(DialogConstants.BUTTON_HEIGHT)
                    .padding(horizontal = DialogConstants.BUTTON_PADDING)
            )
        }
    }
}
@Composable
private fun DialogButton(
    text: String,
    onClick: () -> Unit,
    textColor: Color,
    modifier: Modifier = Modifier
) {
    Button(
        onClick = onClick,
        colors = ButtonDefaults.buttonColors(
            containerColor = Color.White,
            contentColor = textColor
        ),
        shape = RoundedCornerShape(8.dp),
        modifier = modifier
    ) {
        Text(
            text = text,
            fontSize = 16.sp,
            fontWeight = FontWeight.Medium
        )
    }
}
            
            
        

Step 7: Define Styling Constants

Create constants for consistent styling and animation timing.

            
private object DialogConstants {
    val PADDING = 16.dp
    val CONTENT_PADDING = 20.dp
    val CORNER_RADIUS = 16.dp
    val ICON_SIZE = 48.dp
    val ICON_PADDING = 8.dp
    val SPACING = 12.dp
    val BUTTON_PADDING = 8.dp
    val BUTTON_HEIGHT = 48.dp
    val TITLE_FONT_SIZE = 20.sp
    val MESSAGE_FONT_SIZE = 16.sp
    const val ALPHA_DURATION = 300
    const val SCALE_DURATION = 250
    const val ICON_SCALE_DURATION = 350
}
            
            
        

Step 8: Demo Composable

Create a demo screen to showcase the dialog.

            
@Composable
fun GradientWarningDialogDemo() {
    var showDialog by remember { mutableStateOf(false) }
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text(
            text = "Gradient Warning Dialog Demo",
            fontSize = 24.sp,
            fontWeight = FontWeight.Bold,
            textAlign = TextAlign.Center,
            modifier = Modifier.padding(bottom = 8.dp)
        )
        Button(
            onClick = { showDialog = true },
            modifier = Modifier
                .wrapContentWidth()
                .padding(top = 16.dp)
        ) {
            Text("Show Warning Dialog")
        }
        if (showDialog) {
            GradientWarningDialog(
                onDismiss = { showDialog = false },
                onConfirm = { /* Add your confirm logic here */ }
            )
        }
    }
}
            
            
        

Full Source Code

Use the Gradient Warning Dialog UI to provide user-friendly warnings in KMP projects for 2025.

Examples:

  • Display warning dialogs in cross-platform apps
  • Handle critical actions on Android or iOS
  • Provide confirm/cancel options across platforms

Post a Comment

Previous Post Next Post