DEV Community

myougaTheAxo
myougaTheAxo

Posted on

ProgressIndicator in Compose: Loading States, Custom Progress & Overlays

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
)
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

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")
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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")
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)