How to create a gradient SnackBar in Jetpack Compose for Kotlin Multiplatform | KMP

How to Create a Gradient SnackBar in Jetpack Compose for Kotlin Multiplatform

Create a visually appealing Gradient SnackBar in Jetpack Compose, compatible with Kotlin Multiplatform (KMP), to provide user feedback for actions like saving settings or confirming tasks across platforms.

Explore SnackBar Features

Learn to implement a customizable SnackBar with a gradient background, fade and scale animations, and an optional action button, designed for KMP projects.


Key features of the Gradient SnackBar include:
  • Gradient Background: Uses a green linear gradient for visual appeal.
  • Animations: Smooth fade and scale effects for entry.
  • Customizable: Configurable message, action label, and duration.
  • KMP Compatible: Works across Android, iOS, and other platforms with Jetpack Compose.

Step-by-Step Implementation

Follow these steps to create a Gradient SnackBar in your KMP project:

  1. Define the Composable: Create the GradientSnackBar composable with parameters for message, action label, and duration.
  2. Add Animations: Implement fade and scale animations using animateFloatAsState for smooth entry.
  3. Style the Card: Use a Card with a transparent background and rounded corners for the SnackBar container.
  4. Apply Gradient: Add a green linear gradient background to the Row layout.
  5. Include Content: Add an icon, message text, and optional action button with consistent styling.
  6. Create a Demo: Build a demo composable to showcase the SnackBar with a trigger button and auto-dismiss functionality.

Step 1: Define the GradientSnackBar Composable

Start by defining the composable with customizable parameters for message, action label, duration, and a modifier.


@Composable
fun GradientSnackBar(
    message: String = "Action completed successfully!", // Default message for feedback
    actionLabel: String? = "Dismiss", // Optional action button text, nullable for no button
    onAction: () -> Unit = {}, // Callback for action button click
    duration: Long = 3000L, // Duration in milliseconds before auto-dismiss
    modifier: Modifier = Modifier // Modifier for custom styling
) {
    // Composable body will be built in subsequent steps
}
  

Step 2: Add Fade and Scale Animations

Implement animations to make the SnackBar appear smoothly with fade and scale effects.


@Composable
private fun animateFadeAndScaleSnackBar(): Pair {
    val alpha by animateFloatAsState(
        targetValue = 1f, // Fade to full opacity
        animationSpec = tween(
            durationMillis = SnackBarConstants.ALPHA_DURATION, // Duration for fade animation
            easing = FastOutSlowInEasing // Smooth easing for natural effect
        ),
        label = "alpha" // Label for animation debugging
    )
    val scale by animateFloatAsState(
        targetValue = 1f, // Scale to full size
        animationSpec = tween(
            durationMillis = SnackBarConstants.SCALE_DURATION, // Duration for scale animation
            easing = FastOutSlowInEasing // Smooth easing for natural effect
        ),
        label = "scale" // Label for animation debugging
    )
    return alpha to scale // Return pair of animation values
}
  

Step 3: Style the Card Container

Use a Card composable with animations and styling for the SnackBar's container.


Card(
    modifier = modifier
        .fillMaxWidth(0.9f) // Set width to 90% of parent
        .wrapContentHeight() // Adjust height based on content
        .alpha(alphaAnim) // Apply fade animation
        .scale(scaleAnim) // Apply scale animation
        .padding(SnackBarConstants.PADDING), // Add outer padding
    shape = RoundedCornerShape(SnackBarConstants.CORNER_RADIUS), // Rounded corners
    colors = CardDefaults.cardColors(containerColor = Color.Transparent), // Transparent for gradient
    elevation = CardDefaults.cardElevation(defaultElevation = 6.dp) // Shadow for depth
) {
    // Content will be added in the next step
}
  

Step 4: Apply Gradient and Build Row Layout

Add a green gradient background to a Row layout to hold the SnackBar's content.


Row(
    modifier = Modifier
        .background(
            Brush.linearGradient(
                colors = listOf(Color(0xFF4CAF50), Color(0xFF81C784)) // Green gradient
            )
        )
        .padding(SnackBarConstants.CONTENT_PADDING) // Inner padding
        .height(IntrinsicSize.Min), // Minimum height based on content
    verticalAlignment = Alignment.CenterVertically, // Center content vertically
    horizontalArrangement = Arrangement.SpaceBetween // Space icon/message and button
) {
    // Content will be added in the next step
}
  

Step 5: Add Icon, Message, and Action Button

Include an icon, message text, and an optional action button within the Row.


Row(
    verticalAlignment = Alignment.CenterVertically,
    modifier = Modifier.weight(1f) // Take available space
) {
    Icon(
        imageVector = Icons.Rounded.Info, // Info icon
        contentDescription = "Info", // Accessibility description
        tint = Color.White, // White for contrast
        modifier = Modifier
            .size(SnackBarConstants.ICON_SIZE) // Fixed icon size
            .padding(end = SnackBarConstants.ICON_PADDING) // Spacing after icon
    )
    Text(
        text = message, // Provided message
        color = Color.White, // White for contrast
        fontSize = SnackBarConstants.MESSAGE_FONT_SIZE, // Font size
        textAlign = TextAlign.Start, // Left-align
        lineHeight = SnackBarConstants.MESSAGE_FONT_SIZE * 1.2f, // Line height
        modifier = Modifier.padding(end = SnackBarConstants.SPACING) // Spacing
    )
}
actionLabel?.let {
    TextButton(
        onClick = onAction, // Trigger action callback
        modifier = Modifier.height(SnackBarConstants.BUTTON_HEIGHT) // Button height
    ) {
        Text(
            text = it, // Action label
            color = Color.White, // White for contrast
            fontSize = SnackBarConstants.ACTION_FONT_SIZE, // Font size
            fontWeight = FontWeight.Medium // Medium weight
        )
    }
}
  

Step 6: Define Styling Constants and Demo

Create constants for consistent styling and a demo composable to showcase the SnackBar.


private object SnackBarConstants {
    val PADDING = 8.dp // Outer padding
    val CONTENT_PADDING = 12.dp // Inner padding
    val CORNER_RADIUS = 12.dp // Corner radius
    val ICON_SIZE = 24.dp // Icon size
    val ICON_PADDING = 8.dp // Icon padding
    val SPACING = 8.dp // Spacing between elements
    val BUTTON_HEIGHT = 36.dp // Button height
    val MESSAGE_FONT_SIZE = 14.sp // Message text size
    val ACTION_FONT_SIZE = 14.sp // Action text size
    const val ALPHA_DURATION = 250 // Fade duration (ms)
    const val SCALE_DURATION =200 // Scale duration (ms)
}

@Composable
fun GradientSnackBarDemo() {
    var showSnackBar by remember { mutableStateOf(false) } // State for visibility
    Box(
        modifier = Modifier
            .fillMaxSize() // Fill screen
            .padding(16.dp) // Outer padding
    ) {
        Button(
            onClick = { showSnackBar = true }, // Show SnackBar
            modifier = Modifier
                .align(Alignment.Center) // Center button
                .wrapContentWidth() // Adjust width
        ) {
            Text("Show SnackBar") // Button label
        }
        if (showSnackBar) {
            Box(
                modifier = Modifier
                    .align(Alignment.BottomCenter) // Bottom position
                    .padding(bottom = 16.dp) // Bottom padding
            ) {
                GradientSnackBar(
                    message = "Your settings have been saved successfully!", // Demo message
                    actionLabel = "OK", // Action label
                    onAction = { showSnackBar = false }, // Dismiss on action
                    duration = 3000L // Auto-dismiss after 3 seconds
                )
                LaunchedEffect(Unit) {
                    delay(3000L) // Wait for duration
                    showSnackBar = false // Auto-dismiss
                }
            }
        }
    }
}
  

Full Source Code

Below is the complete implementation of the Gradient SnackBar for KMP projects.


package com.boltuix.compose.components

import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Info
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay

// Step 1: Define the GradientSnackBar composable with customizable parameters
@Composable
fun GradientSnackBar(
    message: String = "Action completed successfully!", // Default message for feedback
    actionLabel: String? = "Dismiss", // Optional action button text, nullable for no button
    onAction: () -> Unit = {}, // Callback for action button click
    duration: Long = 3000L, // Duration in milliseconds before auto-dismiss
    modifier: Modifier = Modifier // Modifier for custom styling
) {
    // Step 2: Fetch animation values for fade and scale effects
    val (alphaAnim, scaleAnim) = animateFadeAndScaleSnackBar()

    // Step 3: Create a Card container with animations and styling
    Card(
        modifier = modifier
            .fillMaxWidth(0.9f) // Set width to 90% of parent
            .wrapContentHeight() // Adjust height based on content
            .alpha(alphaAnim) // Apply fade animation
            .scale(scaleAnim) // Apply scale animation
            .padding(SnackBarConstants.PADDING), // Add outer padding
        shape = RoundedCornerShape(SnackBarConstants.CORNER_RADIUS), // Rounded corners
        colors = CardDefaults.cardColors(containerColor = Color.Transparent), // Transparent for gradient
        elevation = CardDefaults.cardElevation(defaultElevation = 6.dp) // Shadow for depth
    ) {
        // Step 4: Build Row layout with gradient background and content
        Row(
            modifier = Modifier
                .background(
                    Brush.linearGradient(
                        colors = listOf(Color(0xFF4CAF50), Color(0xFF81C784)) // Green gradient
                    )
                )
                .padding(SnackBarConstants.CONTENT_PADDING) // Inner padding
                .height(IntrinsicSize.Min), // Minimum height based on content
            verticalAlignment = Alignment.CenterVertically, // Center content vertically
            horizontalArrangement = Arrangement.SpaceBetween // Space icon/message and button
        ) {
            // Step 5: Nested Row for icon and message
            Row(
                verticalAlignment = Alignment.CenterVertically,
                modifier = Modifier.weight(1f) // Take available space
            ) {
                Icon(
                    imageVector = Icons.Rounded.Info, // Info icon
                    contentDescription = "Info", // Accessibility description
                    tint = Color.White, // White for contrast
                    modifier = Modifier
                        .size(SnackBarConstants.ICON_SIZE) // Fixed icon size
                        .padding(end = SnackBarConstants.ICON_PADDING) // Spacing after icon
                )
                Text(
                    text = message, // Provided message
                    color = Color.White, // White for contrast
                    fontSize = SnackBarConstants.MESSAGE_FONT_SIZE, // Font size
                    textAlign = TextAlign.Start, // Left-align
                    lineHeight = SnackBarConstants.MESSAGE_FONT_SIZE * 1.2f, // Line height
                    modifier = Modifier.padding(end = SnackBarConstants.SPACING) // Spacing
                )
            }
            // Step 6: Add optional action button if actionLabel is provided
            actionLabel?.let {
                TextButton(
                    onClick = onAction, // Trigger action callback
                    modifier = Modifier.height(SnackBarConstants.BUTTON_HEIGHT) // Button height
                ) {
                    Text(
                        text = it, // Action label
                        color = Color.White, // White for contrast
                        fontSize = SnackBarConstants.ACTION_FONT_SIZE, // Font size
                        fontWeight = FontWeight.Medium // Medium weight
                    )
                }
            }
        }
    }
}

// Step 7: Implement fade and scale animations for smooth entry
@Composable
private fun animateFadeAndScaleSnackBar(): Pair {
    val alpha by animateFloatAsState(
        targetValue = 1f, // Fade to full opacity
        animationSpec = tween(
            durationMillis = SnackBarConstants.ALPHA_DURATION, // Duration for fade
            easing = FastOutSlowInEasing // Smooth easing
        ),
        label = "alpha" // Label for debugging
    )
    val scale by animateFloatAsState(
        targetValue = 1f, // Scale to full size
        animationSpec = tween(
            durationMillis = SnackBarConstants.SCALE_DURATION, // Duration for scale
            easing = FastOutSlowInEasing // Smooth easing
        ),
        label = "scale" // Label for debugging
    )
    return alpha to scale // Return animation values
}

// Step 8: Define styling constants for consistent design
private object SnackBarConstants {
    val PADDING = 8.dp // Outer padding
    val CONTENT_PADDING = 12.dp // Inner padding
    val CORNER_RADIUS = 12.dp // Corner radius
    val ICON_SIZE = 24.dp // Icon size
    val ICON_PADDING = 8.dp // Icon padding
    val SPACING = 8.dp // Spacing between elements
    val BUTTON_HEIGHT = 36.dp // Button height
    val MESSAGE_FONT_SIZE = 14.sp // Message text size
    val ACTION_FONT_SIZE = 14.sp // Action text size
    const val ALPHA_DURATION = 250 // Fade duration (ms)
    const val SCALE_DURATION = 200 // Scale duration (ms)
}

// Step 9: Demo composable to showcase the SnackBar
@Composable
fun GradientSnackBarDemo() {
    var showSnackBar by remember { mutableStateOf(false) } // State for visibility
    Box(
        modifier = Modifier
            .fillMaxSize() // Fill screen
            .padding(16.dp) // Outer padding
    ) {
        Button(
            onClick = { showSnackBar = true }, // Show SnackBar
            modifier = Modifier
                .align(Alignment.Center) // Center button
                .wrapContentWidth() // Adjust width
        ) {
            Text("Show SnackBar") // Button label
        }
        if (showSnackBar) {
            Box(
                modifier = Modifier
                    .align(Alignment.BottomCenter) // Bottom position
                    .padding(bottom = 16.dp) // Bottom padding
            ) {
                GradientSnackBar(
                    message = "Your settings have been saved successfully!", // Demo message
                    actionLabel = "OK", // Action label
                    onAction = { showSnackBar = false }, // Dismiss on action
                    duration = 3000L // Auto-dismiss after 3 seconds
                )
                LaunchedEffect(Unit) {
                    delay(3000L) // Wait for duration
                    showSnackBar = false // Auto-dismiss
                }
            }
        }
    }
}
  

Use the Gradient SnackBar to provide concise feedback for user actions with a modern, animated design in KMP projects.

Examples:

  • Confirm settings saved in a cross-platform app
  • Notify item added to a shopping cart on Android or iOS
  • Alert successful form submission across platforms

Post a Comment

Previous Post Next Post