A Compose Multiplatform pull-to-refresh that uses a Lottie animation as the refresh indicator. Targets Android and iOS, powered by Compottie for cross-platform Lottie rendering.
This is a from-scratch, pure-Compose re-imagining of the classic view-based
LottieSwipeRefreshLayout β no Android
View system, no Accompanist. Just a NestedScrollConnection and a Lottie painter, so it runs
unchanged on every Compose Multiplatform target.
Pull down β the Lottie animation scrubs 0β1 with your finger β release past the threshold β it loops while refreshing.
- π― Pure Compose β works on Android & iOS from a single
commonMainimplementation. - πͺ Lottie indicator β the animation scrubs live with your finger as you pull, then loops while refreshing.
- π§² Sticky drag physics β the same resistance feel as the original view library.
- π§© Composable-first API β hoistable state (
LottiePullRefreshState), custom-indicator slot, and convenience overloads that take aLottieCompositionor a raw Lottie JSON string. - π¨ Customizable β threshold distance, indicator size, circular background, elevation, alignment.
- πͺ Overlay or push-down β
indicatorOverlay = truefloats the indicator over the content;falsepushes the content down so the indicator sits in the gap above it.
The Lottie scrubs 0β1 with the drag β it plays forward as you pull down and reverses as you pull back up (the progress is bound directly to the pull distance), then loops while refreshing. Use a vector Lottie (shapes only); image-backed or precomp/matte-heavy files won't render correctly in Compottie. The sample demonstrates several styles (a circular butterfly, a monitor-progress ring, a background-less dot spinner, and a push-down variant).
1. Add the JitPack repository to your root settings.gradle.kts:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven { url = uri("https://jitpack.io") } // <-- add this
}
}2. Add the dependency. In a Compose Multiplatform module, put it in commonMain:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("com.github.Micoder-dev.LottiePullRefresh:lottie-pullrefresh:0.1.0")
}
}
}For an Android-only project it's the same coordinate in your dependencies { } block:
dependencies {
implementation("com.github.Micoder-dev.LottiePullRefresh:lottie-pullrefresh:0.1.0")
}iOS note. JitPack builds on Linux, which cannot compile Kotlin/Native Apple targets, so the JitPack artifact ships the Android variant. The full Android + iOS build is published from macOS to Maven Central under
io.github.micoder:lottie-pullrefresh. For an iOS/KMP app, consume it from Maven Central; for Android, JitPack is all you need.
Compottie is exposed as an api dependency, so rememberLottieComposition, LottieCompositionSpec,
etc. are available to you without an extra dependency.
The simplest overload takes the raw Lottie JSON and handles everything for you:
@Composable
fun Feed(viewModel: FeedViewModel) {
val isRefreshing by viewModel.isRefreshing.collectAsState()
// Load the animation JSON however you like (compose resources, network, assetsβ¦).
val json = /* "{ \"v\": \"5.7.4\", ... }" */
LottiePullRefresh(
isRefreshing = isRefreshing,
onRefresh = { viewModel.refresh() },
lottieJson = json,
) {
LazyColumn(Modifier.fillMaxSize()) {
items(viewModel.items) { ItemRow(it) }
}
}
}val composition by rememberLottieComposition {
LottieCompositionSpec.JsonString(json)
}
LottiePullRefresh(
isRefreshing = isRefreshing,
onRefresh = { refresh() },
composition = composition,
refreshThreshold = 96.dp,
indicatorSize = 56.dp,
) {
// content
}See Modes & styles for the overlay vs push-down modes and the background/size options.
Hoist the state and drive your own indicator via the low-level overload:
val state = rememberLottiePullRefreshState(isRefreshing)
LottiePullRefresh(
isRefreshing = isRefreshing,
onRefresh = { refresh() },
state = state,
indicator = { s, triggerDistance ->
LottieRefreshIndicator(
state = s,
refreshTriggerDistance = triggerDistance,
composition = composition,
background = false, // draw the raw animation, no circular surface
indicatorSize = 72.dp,
)
},
) {
// content
}LottiePullRefreshState exposes:
| member | description |
|---|---|
isRefreshing |
whether a refresh is in progress |
isSwipeInProgress |
whether the user is actively dragging |
indicatorOffset |
current indicator offset in pixels (drive custom visuals off this) |
The indicator floats over the content. Swap in any vector Lottie and it scrubs 0β1 with the pull.
Set indicatorOverlay = false and the content is pushed down by the pull distance, so the
indicator sits in the gap above it (like the classic Material swipe-refresh).
| Declaration | Purpose |
|---|---|
LottiePullRefresh(isRefreshing, onRefresh, lottieJson, β¦) |
convenience β loads the JSON for you |
LottiePullRefresh(isRefreshing, onRefresh, composition, β¦) |
pass a pre-built LottieComposition |
LottiePullRefresh(isRefreshing, onRefresh, indicator, β¦) |
low-level β bring your own indicator |
LottieRefreshIndicator(state, refreshTriggerDistance, composition, β¦) |
the default indicator |
rememberLottiePullRefreshState(isRefreshing) |
remembers/updates the state |
LottiePullRefresh/
βββ lottie-pullrefresh/ # the library (Android + iOS)
β βββ src/commonMain/kotlin/io/github/micoder/lottiepullrefresh/
β βββ LottiePullRefresh.kt # the container composable + overloads
β βββ LottieRefreshIndicator.kt # the default Lottie indicator
β βββ LottiePullRefreshState.kt # hoistable state
β βββ LottiePullRefreshNestedScrollConnection.kt # gesture/physics
βββ sample/composeApp/ # Android + iOS sample app
βββ src/commonMain/ # shared App() + bundled loader.json
βββ src/androidMain/ # MainActivity
βββ src/iosMain/ # MainViewController()
Android
./gradlew :sample:composeApp:assembleDebug
# then install the APK from sample/composeApp/build/outputs/apk/debug/iOS β the shared UI is exposed to Swift via MainViewController() in
sample/composeApp/src/iosMain. Create an Xcode app that embeds the ComposeApp framework and host
it in a SwiftUI UIViewControllerRepresentable:
struct ComposeView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
MainViewControllerKt.MainViewController()
}
func updateUIViewController(_ vc: UIViewController, context: Context) {}
}iOS frameworks can only be compiled on macOS with Xcode; the Android artifacts build on any platform.
- Kotlin 2.2.21 Β· Compose Multiplatform 1.11.1 Β· AGP 8.11.0 Β· Gradle 8.13
- Compottie 2.2.4
- Library
minSdk21 Β· sampleminSdk24 (Compose resources requirement)
Copyright 2026 Micoder-dev
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- Original view-based library: nabil6391/LottieSwipeRefreshLayout
- Gesture model inspired by Google's Accompanist SwipeRefresh (Apache-2.0)
- Cross-platform Lottie rendering: alexzhirkevich/compottie


