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.
- 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:
- Define the Composable: Create
NoInternetScreen
with parameters for title, subtitle, image, modifier, and retry callback. - Animate Fade and Scale: Use
animateFloatAsState
for alpha and scale animations. - Build Main Layout: Use
BoxWithConstraints
to detect orientation and apply animations. - Conditional Layout: Row for landscape, Column for portrait, with image and text sections.
- Image Section: Display the image with size and padding.
- Text Section: Add title, subtitle, spacer, and retry button.
- 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