ProgressIndicator in Compose: Loading States, Custom Progress & Overlays
Loading states are critical for app UX. Jetpack Compose provides built-in progress indicators plus Canvas APIs for advanced custom designs. Let's master linear, circular, and overlay patterns.
1. CircularProgressIndicator: Indeterminate Loading
For "waiting" states without progress info:
CircularProgressIndicator(
modifier = Modifier.size(48.dp),
color = MaterialTheme.colorScheme.primary,
strokeWidth = 4.dp
)
The indicator spins indefinitely. Wrap it in a Box with padding for layout:
Box(
modifier = Modifier
.size(120.dp)
.background(MaterialTheme.colorScheme.surface),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
2. CircularProgressIndicator: Determinate Progress
Show actual progress with a percentage value:
var progress by remember { mutableStateOf(0.3f) }
CircularProgressIndicator(
progress = { progress },
modifier = Modifier.size(60.dp),
color = MaterialTheme.colorScheme.secondary
)
// Animate progress over time
LaunchedEffect(Unit) {
repeat(100) {
progress = (it + 1) / 100f
delay(50)
}
}
3. LinearProgressIndicator: File Downloads & Uploads
For horizontal progress bars:
// Indeterminate
LinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.height(4.dp)
)
// Determinate (e.g., file upload at 65%)
LinearProgressIndicator(
progress = { 0.65f },
modifier = Modifier
.fillMaxWidth()
.height(6.dp),
color = MaterialTheme.colorScheme.tertiary
)
4. Animated Progress with animateFloatAsState
Smooth transitions between progress values:
var targetProgress by remember { mutableStateOf(0f) }
val animatedProgress by animateFloatAsState(
targetValue = targetProgress,
animationSpec = tween(durationMillis = 500),
label = "Progress"
)
CircularProgressIndicator(
progress = { animatedProgress },
modifier = Modifier.size(64.dp)
)
// Trigger animation
Button(onClick = { targetProgress += 0.1f }) {
Text("Increase")
}
5. Custom Canvas Circular Progress with Text
For branded designs, draw progress directly on Canvas:
@Composable
fun CustomCircularProgress(
progress: Float,
size: Dp = 120.dp,
strokeWidth: Dp = 8.dp,
backgroundColor: Color = Color.LightGray,
progressColor: Color = Color.Blue
) {
Box(
modifier = Modifier.size(size),
contentAlignment = Alignment.Center
) {
Canvas(modifier = Modifier.size(size)) {
val radius = (size.toPx() / 2) - (strokeWidth.toPx() / 2)
val center = Offset(size.toPx() / 2, size.toPx() / 2)
// Background circle
drawCircle(
color = backgroundColor,
radius = radius,
center = center,
style = Stroke(strokeWidth.toPx())
)
// Progress arc
drawArc(
color = progressColor,
startAngle = -90f,
sweepAngle = progress * 360f,
useCenter = false,
topLeft = Offset(center.x - radius, center.y - radius),
size = Size(radius * 2, radius * 2),
style = Stroke(strokeWidth.toPx())
)
}
// Center text
Text(
"${(progress * 100).toInt()}%",
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold
)
}
}
// Usage
var fileProgress by remember { mutableStateOf(0.45f) }
CustomCircularProgress(fileProgress)
6. Loading Overlay Pattern
Disable UI while loading:
@Composable
fun LoadingOverlay(
isLoading: Boolean,
content: @Composable () -> Unit
) {
Box {
content()
if (isLoading) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.3f))
.clickable(enabled = true) {},
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
modifier = Modifier.size(48.dp)
)
}
}
}
}
// Usage
var isLoading by remember { mutableStateOf(false) }
LoadingOverlay(isLoading) {
Button(onClick = { isLoading = true }) {
Text("Submit")
}
}
7. Loading Button State
Combine button with progress indicator:
@Composable
fun LoadingButton(
text: String,
isLoading: Boolean,
onClick: () -> Unit
) {
Button(
onClick = onClick,
enabled = !isLoading
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(18.dp),
color = Color.White,
strokeWidth = 2.dp
)
Spacer(modifier = Modifier.width(8.dp))
}
Text(text)
}
}
// Usage
var isSubmitting by remember { mutableStateOf(false) }
LoadingButton("Submit", isSubmitting) {
isSubmitting = true
// Async work
}
Best Practices
- Indeterminate for unknown duration (API calls, file uploads of unknown size)
- Determinate for known progress (file downloads with size, multi-step forms)
- Keep animations smooth (300-500ms transitions feel responsive)
- Overlay only for critical actions (avoid blocking all interaction)
- Provide cancel buttons where possible (long operations need escape routes)
- Test with accessibility - ensure progress updates announce to screen readers
- Custom Canvas for brand consistency - Material3 defaults work, but custom designs stand out
Progress indicators aren't just spinners—they're trust builders. They communicate that your app is working and keeps users engaged during waits.
8 Android App Templates → https://myougatheaxo.gumroad.com
Top comments (0)