How to create a Empty State in Jetpack Compose for Kotlin Multiplatform

Empty State KMP

Create a responsive empty state UI for no internet connection in Jetpack Compose, compatible with Kotlin Multiplatform (KMP), featuring animations, adaptive layout for orientations, and retry functionality.

Explore Empty State UI Features

Learn to implement a customizable responsive empty state UI with fade-in and scale animations, resource-based strings and images, and orientation-aware design, optimized for KMP projects in 2025.


Key features of the Responsive Empty State UI include:
  • Responsive Layout: Automatically adapts to portrait and landscape orientations using BoxWithConstraints.
  • Animations: Smooth fade-in and scale-in effects for engaging user experience.
  • Customizable: Parameters for title, subtitle, image, and retry action.
  • KMP Compatible: Utilizes Compose Multiplatform resources for cross-platform support.

Step-by-Step Implementation

Follow these steps to create a Responsive Empty State UI in your KMP project:

  1. Define the Composable: Create NoInternetScreen with parameters for title, subtitle, image, modifier, and retry callback.
  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. Conditional Layout: Row for landscape, Column for portrait, with image and text sections.
  5. Image Section: Display the image with size and padding.
  6. Text Section: Add title, subtitle, spacer, and retry button.
  7. Define Constants: Create object for styling and animation durations.

Step 1: Define the NoInternetScreen Composable

Start by defining the composable with customizable parameters using multiplatform resources.


@Composable
fun NoInternetScreen(
    title: String = stringResource(Res.string.no_internet_title), // Title from resources
    subtitle: String = stringResource(Res.string.no_internet_subtitle), // Subtitle from resources
    imageRes: Painter = painterResource(Res.drawable.empty_state_no_internet_red), // Default image
    modifier: Modifier = Modifier,
    onRetry: () -> Unit = {} // 🔁 Retry action callback
) {
    val (alphaAnim, scaleAnim) = animateFadeAndScale() // Load entrance animation values

    // Step 3 and beyond here
}
  

Step 2: Animate Fade and Scale

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


@Composable
internal fun animateFadeAndScale(): Pair<Float, Float> {
    // 🔄 Animate alpha from 0 to 1 for fade-in
    val alpha by animateFloatAsState(
        targetValue = 1f,
        animationSpec = tween(
            durationMillis = NoInternetConstants.ALPHA_DURATION,
            easing = FastOutSlowInEasing
        ),
        label = "alpha"
    )

    // 🔄 Animate scale from 0 to 1 for zoom-in
    val scale by animateFloatAsState(
        targetValue = 1f,
        animationSpec = tween(
            durationMillis = NoInternetConstants.SCALE_DURATION,
            easing = FastOutSlowInEasing
        ),
        label = "scale"
    )

    return alpha to scale
}
  

Step 3: Build Main Layout

Use BoxWithConstraints for orientation detection and apply background, padding, and animations.


BoxWithConstraints(
    modifier = modifier
        .fillMaxSize()
        .background(MaterialTheme.colorScheme.background) // Apply background color
        .padding(NoInternetConstants.PADDING) // Apply padding
) {
    val isLandscape = maxWidth > maxHeight // Detect screen orientation

    val layoutModifier = Modifier
        .fillMaxSize()
        .alpha(alphaAnim) // Fade in effect
        .scale(scaleAnim) // Zoom-in effect

    // Step 4 here
}
  

Step 4: Conditional Layout

Switch between Row and Column based on orientation for responsive design.


// 🔄 Dynamic layout based on orientation
if (isLandscape) {
    Row(
        modifier = layoutModifier,
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.SpaceEvenly
    ) {
        ImageSection(imageRes) // Display image
        TextSection(title, subtitle, onRetry) // Show texts and retry button
    }
} else {
    Column(
        modifier = layoutModifier,
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        ImageSection(imageRes)
        Spacer(modifier = Modifier.height(NoInternetConstants.TEXT_SPACING * 2)) // Spacing
        TextSection(title, subtitle, onRetry)
    }
}
  

Step 5: Image Section

Display the image with content description for accessibility.


@Composable
private fun ImageSection(imageRes: Painter) {
    Image(
        painter = imageRes,
        contentDescription = stringResource(Res.string.no_internet_image_description), // Accessibility label
        modifier = Modifier
            .size(NoInternetConstants.IMAGE_SIZE) // Image size
            .padding(end = NoInternetConstants.IMAGE_PADDING) // Padding to the right
    )
}
  

Step 6: Text Section

Add title, subtitle, and retry button with styling.


@Composable
private fun TextSection(title: String, subtitle: String, onRetry: () -> Unit) {
    Column(
        modifier = Modifier
            .padding(horizontal = NoInternetConstants.TEXT_HORIZONTAL_PADDING), // Text padding
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(
            text = title,
            fontSize = NoInternetConstants.TITLE_FONT_SIZE, // Title font size
            fontWeight = FontWeight.Medium,
            color = MaterialTheme.colorScheme.onBackground,
            textAlign = TextAlign.Center
        )
        Spacer(modifier = Modifier.height(NoInternetConstants.TEXT_SPACING))
        Text(
            text = subtitle,
            fontSize = NoInternetConstants.SUBTITLE_FONT_SIZE,
            color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f),
            textAlign = TextAlign.Center
        )
        Spacer(modifier = Modifier.height(NoInternetConstants.TEXT_SPACING * 2))

        // 🔁 Retry Button
        Button(
            onClick = onRetry,
            colors = ButtonDefaults.buttonColors(
                containerColor = MaterialTheme.colorScheme.onBackground,
                contentColor = MaterialTheme.colorScheme.background
            )
        ) {
            Text(text = stringResource(Res.string.no_internet_button)) // Button label
        }
    }
}
  

Step 7: Define Styling Constants

Create constants for consistent styling and animation timing.


// Constants for layout styling and animation timing
private object NoInternetConstants {

    // Padding around the entire layout
    val PADDING = 32.dp

    // Size of the image (width x height)
    val IMAGE_SIZE = 300.dp

    // Space to the right of the image
    val IMAGE_PADDING = 16.dp

    // Horizontal padding for text content
    val TEXT_HORIZONTAL_PADDING = 16.dp

    // Spacing between text elements
    val TEXT_SPACING = 8.dp

    // Font size for the title text
    val TITLE_FONT_SIZE = 20.sp

    // Font size for the subtitle text
    val SUBTITLE_FONT_SIZE = 16.sp

    // Fade-in animation duration (milliseconds)
    const val ALPHA_DURATION = 800

    // Scale-in animation duration (milliseconds)
    const val SCALE_DURATION = 600
}
  

Full Source Code

Use the Responsive Empty State UI to handle no internet scenarios with a user-friendly, animated design in KMP projects for 2025.

Examples:

  • Display no connection screen in a cross-platform app
  • Handle offline mode on Android or iOS
  • Provide retry option across platforms

Post a Comment

Previous Post Next Post