Most Android advice about loading indicators starts in the wrong place. People search for a loading spinner Android guide and get tutorials for the Spinner widget, which is a dropdown selector, not a loading UI at all. That confusion wastes time, and it still shows up in older docs, forum answers, and copy-paste snippets that never explain what should ship in a modern app.
The right model is simpler. Use an indeterminate progress indicator when work is happening but completion time is unknown, use a determinate progress bar when the app can show advancement, and skip the indicator entirely when the wait is so short that the UI would just flash. Android’s own loading-state guidance says a spinner flash under 300 milliseconds feels like a glitch, while spinner usage makes the most sense around 1 to 2 seconds and beyond that you should reach for something more informative such as skeleton screens or progress feedback. Material Design 3 says the same thing in different words, pick the indicator based on the expected wait, not on habit. Android loading-state guidance and Material Design 3 loading indicator guidance both point in that direction.
Table of Contents
- The Android Spinner Terminology Problem
- Building Circular Loading Spinners in XML and Code
- Material 3 and Jetpack Compose Loading Indicators
- Animated Loading Alternatives with Lottie
- Timing Thresholds and UX Placement Patterns
- Choosing the Right Loading Pattern for Every Scenario
The Android Spinner Terminology Problem
The most common mistake is also the most avoidable one. Android Spinner is a dropdown widget, not a loading indicator, so a search for a circular loader that leads to Spinner tutorials indicates the wrong API family. That naming collision explains why so many articles mix up Spinner, ProgressBar, CircularProgressIndicator, and generic “loading circle” language without ever separating the UI selector from the progress UI.

What developers actually need
If the task is unknown-duration work, the class family you want is the progress indicator family. In classic XML layouts, that usually means ProgressBar in indeterminate mode. In modern Material terms, you’re usually looking for CircularProgressIndicator when the wait is open-ended and the user just needs confirmation that the app is alive and processing.
That distinction matters because the UX goal changes with the component. A dropdown Spinner asks for a choice. A loading spinner communicates that the app is busy. Mixing those up creates bad search intent, but it also creates bad code reviews, because the team ends up discussing the wrong widget entirely.
Practical rule: if the user must choose from a list, use the Spinner widget. If the app is waiting on work, use a progress indicator.
Why the confusion persists
Older Android content still leans on the built-in ProgressBar widget and generic “spinner” language, which is why the term never fully went away. Community guidance has also long treated the loader as a temporary overlay during network calls, often with cancelable false, which reinforces the idea that the spinner is just a visual waiting state rather than a feature with clear naming and timing rules. The older pattern still exists, but it’s not the same thing as the dropdown widget. Android community guidance on loading flows captures that legacy clearly.
When you search today, be precise. Use queries like CircularProgressIndicator Android, indeterminate ProgressBar, or Material 3 loading indicator. That gets you to the current component names faster, and it avoids a pile of irrelevant dropdown examples that were never meant for loading state at all.
Building Circular Loading Spinners in XML and Code

The classic setup is still the fastest way to ship a reliable loading state in a View-based app. A circular spinner inside a layout, toggled with visibility, covers most async operations cleanly as long as you keep the UI thread free and the state source of truth somewhere predictable. The spinner itself should never be the thing doing work, it should only reflect that work is already happening.
XML layout setup
A simple indeterminate loader in XML is enough for many screens. The important part is not the widget declaration, it’s the layout strategy around it. Put the spinner in a container that can appear above content without forcing the whole screen to jump around when it toggles.
A practical XML pattern looks like this:
<FrameLayout
android:id="@+id/loadingContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone">
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true" />
</FrameLayout>
That gives you a centered, circular indeterminate spinner that can be shown while the rest of the UI stays intact. If you need reuse, wrap it in a small custom view or a composite layout instead of sprinkling raw ProgressBar references across fragments.
Kotlin and Java control
The control flow should be boring, because boring loading code is good loading code. Keep the spinner visible only while a network call, database fetch, or background task is unresolved, then hide it the moment the response is ready. If you’re using ViewBinding, you avoid a lot of brittle findViewById references and keep the code much easier to audit.
A clean Kotlin pattern is:
binding.loadingContainer.isVisible = true
viewModel.loadData().invokeOnCompletion {
binding.loadingContainer.isVisible = false
}
If you’re still in Java, the shape is the same. Show the container before the async work starts, hide it in the success path, and also hide it in the failure path so a silent exception doesn’t trap the user behind a permanent spinner. For teams comparing timing and load-state behavior across releases, a change log like the page timing change log can help you spot when perceived performance changes even if the backend barely moved.
A lot of teams make one avoidable mistake here, they block the main thread and then wonder why the spinner freezes. The spinner isn’t a substitute for proper threading. If you’re doing network or disk work directly on the UI thread, the animation can’t save you, and the app will feel broken no matter how polished the indicator looks.
Material 3 and Jetpack Compose Loading Indicators
Material 3 and Compose changed how Android apps should present loading state. The spinner is no longer something you babysit as a separate widget, because the better pattern is to treat loading as part of screen state, theming, and composition. That makes the UI easier to keep aligned when loading, content, and error states all need to swap in and out without leaving stale views behind.
XML Material components versus Compose
| Feature | Material 3 XML | Jetpack Compose |
|---|---|---|
| State model | View visibility and binding logic | Reactive state from StateFlow or LiveData |
| Theming | XML styles, theme attributes, color resources | MaterialTheme, composable color roles, dynamic color |
| Code shape | Imperative show and hide calls | Declarative UI based on state |
| Reuse | Custom views or layout wrappers | Higher-order composables and reusable state slots |
| Loading transitions | Manual animation and container swaps | Smooth state-driven recomposition |
The XML path still holds up if your app is mostly View-based or you are maintaining older screens. Compose fits better when loading is only one branch of a screen that already reacts to data, because the UI can describe what should exist instead of manually toggling what should disappear. That difference shows up fast on screens with several async sources, where one region may still be loading while another is already ready.
Compose implementation details
Compose loading indicators are straightforward, but the state wiring matters more than the component call itself. A CircularProgressIndicator inside a conditional branch is usually enough, and you can tie it to a StateFlow<Boolean> in the ViewModel so the spinner appears only while work is pending. If the request is long-lived, keep the indicator local to the affected region instead of blocking the whole page.
A simple structure is:
if (uiState.isLoading) {
CircularProgressIndicator()
} else {
ContentView(data = uiState.data)
}
That pattern scales well once error and empty states are added, because each state can render on its own instead of being layered through a pile of mutable view calls. For teams that prototype screens before the Android code is wired up, component composition patterns can help clarify how loading, empty, and ready states fit together in a reusable layout model. If you are trying to choose the right mobile builder, the same state-first thinking applies, even though the runtime target is different.
Practical rule: let Compose react to state. Do not manually animate around missing state modeling.
Material 3 also gives you a cleaner baseline for indicator styling, especially if the rest of the app already uses dynamic color and surface roles. That is the part many older tutorials skip. They show the widget call, but they do not explain how the indicator should inherit theme color, container contrast, and layout spacing so it still reads correctly in dark mode and in dense app bars. For the official guidance on indicator behavior and sizing, use the Material Design 3 loading indicator guidelines and then map that guidance onto your own screen hierarchy.
If you are choosing between XML and Compose, the deciding factor is usually the rest of the screen, not the spinner itself. A Material 3 loader inside Compose tends to feel cleaner because the state source, the theme, and the indicator all sit in the same model. That consistency lowers the odds that a spinner outlives the request that created it, which is one of the fastest ways to make an Android app feel sloppy.
Animated Loading Alternatives with Lottie
A plain spinner is functional, but it’s not always the right visual. Branded loading states can make sense when the user is waiting on a full-screen transition, a heavy content load, or a moment where the app’s personality matters more than strict minimalism. Lottie is the obvious Android option when you want motion that looks designed rather than generic.
Integration and control
Lottie works best when the animation stays lightweight and looped intentionally. The setup is usually simple, add the dependency, drop in a JSON asset, and control playback through the view or composable wrapper that owns the loading surface. What matters is not that the animation exists, but that it doesn’t become a performance tax on screens that are already busy.
Use it like this in practice:
- Full-screen states: show a branded animation when the user is entering a section that really needs a visual hold.
- Inline actions: keep the standard spinner for form submissions, save actions, and refresh gestures.
- Button-level waits: use a small indicator inside or beside the button instead of a decorative animation that distracts from the action itself.
- Repeatable loops: keep playback calm and predictable, not noisy or attention-seeking.
When Lottie is worth it
The trade-off is simple. Lottie consumes more memory and CPU than a native ProgressBar, so the payoff has to justify the extra work. If the user only needs a quick signal that something is happening, a native spinner is still the better fit. If the loading moment is part of the product’s identity, the animation can earn its keep.
A good benchmark is whether the animation helps the user wait more comfortably without hiding the fact that they’re waiting. If the answer is no, the motion is decoration, not UX. For teams building branded mobile experiences through external tools, the choose the right mobile builder discussion is useful because it forces the same question before implementation, not after.
Timing Thresholds and UX Placement Patterns
Timing decides whether a loading spinner feels helpful or sloppy. Show it too early and the screen looks twitchy. Leave it up too long and the app feels stalled. The right move is to match the indicator to the delay the user is likely to notice, not to your preference for motion.

Timing thresholds that actually hold up
Android guidance is clear about short waits. Do not show a loading state for work under 300 milliseconds, because the flash itself reads like a glitch. Spinners fit short backend actions and tend to work well for waits around 1 to 2 seconds, while longer waits need clearer progress feedback or a different pattern entirely. That approach keeps the UI from reacting to ordinary latency as if something is broken.
Practical rule: if the user cannot tell whether the action happened instantly, do not force a spinner into the gap.
In practice, that breaks down like this:
- Under 300 milliseconds: show nothing.
- Around 1 to 2 seconds: use a subtle spinner.
- Longer waits: switch to a progress bar, skeleton screen, or text that explains what is happening.
A loading spinner is a signal, not a scoreboard. Material Design 3 recommends choosing the indicator based on the wait and using other patterns when the app can communicate advancement more clearly. Material 3 loading indicator guidance is the clearest official reference for that split.
Placement patterns that feel natural
Placement matters just as much as timing. A full-screen overlay works when the whole surface is unavailable, such as during blocking authentication or first-load bootstrapping. Inline spinners fit feeds, lists, and partial refreshes better, because the rest of the screen can keep working. Button-level loaders are the cleanest option for submit actions, since they localize the wait to the control that triggered it.
That same logic is why micro-feedback has to stay tied to the interaction surface. micro-interaction design is useful here because loaders are part of a broader feedback loop, not a standalone animation choice. If the motion does not clarify what changed, it is doing extra work without adding value.
Accessibility needs to be deliberate too. Give the indicator a clear content description when it conveys state, and avoid announcing trivial refreshes over and over. For teams that need practical examples of inclusive states, accessible loading states in Expo is a useful reference point. If motion sensitivity is a concern, reduce decorative animation and keep the state change obvious through text or layout instead of relying on movement alone.
Choosing the Right Loading Pattern for Every Scenario
A good Android app doesn’t use one loader everywhere. It uses the smallest indicator that matches the wait, the scope, and the amount of progress information available. That’s the whole decision tree, and it’s why a spinner is sometimes perfect and sometimes the wrong answer.

The practical mapping
If the task is a quick network request with no visible progress, use a spinner. If the app can report progress, use a determinate progress bar. If the wait is mostly about perceived responsiveness, a skeleton screen can feel better because it preserves layout structure while content streams in. That distinction is why react loading bar patterns translate so well conceptually into Android, even when the runtime implementation differs.
Common scenarios map cleanly:
- Initial app loading: use a full-screen state, not a tiny spinner floating in empty space.
- Database query with unknown duration: use an indeterminate spinner or skeleton, depending on whether the layout is already known.
- File upload: use determinate progress whenever bytes can be tracked.
- Pagination and list refresh: prefer inline spinners or content placeholders, not a modal blocker.
- Long-running sync: show progress details or staged status, because a bare spinner gets old fast.
The mistakes are usually obvious once you name them. Teams stack multiple spinners on top of each other, forget the error state, or leave the loader visible after a silent failure. I’ve also seen apps keep the spinner up while the content is already usable, which teaches users to ignore feedback entirely.
Test loading behavior under slow networks, flaky connections, older devices, and accessibility settings. If the UI still feels calm, specific, and honest under those conditions, the loading pattern is doing its job.
DOM Studio helps teams design and assemble polished UI components, including interfaces that need clear loading states, before they ever touch production Android code. If you’re working through spinner, skeleton, and progress patterns for a product interface, visit DOM Studio and use it to shape the component structure, then carry the same clarity into your Android implementation.
