
Implements a stale-while-revalidate (SWR) cache invalidation strategy for Compose, enabling efficient data fetching, error handling, and auto revalidation, closely mirroring React SWR's API and options.
This library is inspired by React SWR ported for Jetpack Compose and Compose Multiplatform.
Supported platforms are Android, Desktop, iOS and Web.
The API specification of React SWR is followed as much as possible.
Options are also supported for the most part.
According to React SWR, "SWR" refers to
The name “SWR” is derived from stale-while-revalidate, a HTTP cache invalidation strategy popularized by HTTP RFC 5861. SWR is a strategy to first return the data from cache (stale), then send the fetch request (revalidate), and finally come with the up-to-date data.
Add the following gradle dependency exchanging *.*.* for the latest release.
implementation("com.kazakago.swr:swr-compose:*.*.*")As with React SWR, implement the "fetcher function" and set it with the key to rememberSWR().
Using Kotlin's Destructuring declarations for return values can be written in the same way as in React SWR.
private val fetcher: suspend (key: String) -> String = {
getNameApi.execute(key)
}
@Composable
fun Profile() {
val (data, error) = rememberSWR("/api/user", fetcher)
if (error != null) {
Text("failed to load")
} else if (data == null) {
Text("loading...")
} else {
Text("hello $data!")
}
}Live demo of Kotlin/Wasm is here.
Refer to the example module for details. This module works as a Compose Multiplatform app.
| Feature name | Note |
|---|---|
| Global Configuration | |
| Data Fetching | |
| Error Handling | |
| Auto Revalidation | |
| Conditional Data Fetching | |
| Arguments | |
| Mutation | Available by rememberSWRMutation()
|
| Pagination | Available by rememberSWRInfinite()
|
| Prefetching Data | Available by rememberSWRPreload()
|
| Subscription | Available by rememberSWRSubscription()
|
The following options are supported for React SWR.
https://swr.vercel.app/docs/options
| Option name | Default value | Description |
|---|---|---|
| revalidateIfStale | true | Revalidate when cached data is stale |
| revalidateOnMount | true | Revalidate when component mounts |
| revalidateOnFocus | true | Revalidate when app regains focus |
| revalidateOnReconnect | true | Revalidate when network reconnects |
| refreshInterval | 0.seconds | Polling interval for periodic refresh (0=disabled) |
| refreshWhenHidden | false | Continue polling when app is in background |
| refreshWhenOffline | false | Continue polling when offline |
| shouldRetryOnError | true | Retry fetching on error |
| dedupingInterval | 2.seconds | Time window to deduplicate identical requests |
| focusThrottleInterval | 5.seconds | Min interval between focus-triggered revalidations |
| loadingTimeout | 3.seconds | Timeout before onLoadingSlow callback fires |
| errorRetryInterval | 5.seconds | Wait time between error retries |
| errorRetryCount | Maximum number of error retries | |
| fallbackData | Data to display while loading | |
| onLoadingSlow | Callback when loading exceeds loadingTimeout
|
|
| onSuccess | Callback on successful data fetch | |
| onError | Callback on fetch error | |
| keepPreviousData | false | Keep previous data when key changes |
| isPaused | Pause all revalidation when returns true | |
| onErrorRetry | Exponential backoff | Custom error retry handler |
rememberSWRMutation() provides a way to mutate remote data. Unlike rememberSWR(), it does not automatically fetch data — mutations are triggered manually.
private val fetcher: suspend (key: String, arg: String) -> String = { key, arg ->
updateNameApi.execute(key, arg)
}
@Composable
fun UpdateProfile() {
val (trigger, isMutating) = rememberSWRMutation("/api/user", fetcher)
Button(
onClick = { scope.launch { trigger("new name") } },
enabled = !isMutating,
) {
Text(if (isMutating) "Updating..." else "Update")
}
}| Option name | Default value | Description |
|---|---|---|
| optimisticData | Data to display immediately before mutation resolves | |
| revalidate | false | Revalidate cached data after mutation |
| populateCache | false | Update cache with mutation result |
| rollbackOnError | true | Revert to previous data if mutation fails |
| onSuccess | Callback on successful mutation | |
| onError | Callback on mutation error |
rememberSWRSubscription() integrates with real-time data sources such as WebSockets, Server-Sent Events, or Firestore. Data received via the subscription is written to the SWR cache, so any rememberSWR() using the same key automatically displays the latest data.
@Composable
fun LiveFeed() {
val (data, error) = rememberSWRSubscription(
key = "/ws/feed",
subscribe = { key ->
callbackFlow {
val ws = WebSocket(key) { message ->
trySend(message)
}
awaitClose { ws.close() }
}
},
)
if (error != null) {
Text("error: ${error.message}")
} else {
Text("latest: ${data ?: "..."}")
}
}rememberSWRInfinite() provides paginated data fetching with dynamic page management.
@Composable
fun UserList() {
val (data, error, isValidating, isLoading, _, size, setSize) = rememberSWRInfinite(
getKey = { pageIndex, previousPageData ->
if (previousPageData != null && previousPageData.isEmpty()) null // reached the end
else "/api/users?page=$pageIndex"
},
fetcher = { key -> fetchUsers(key) },
)
// data is List<List<User>?> — one entry per page
data?.flatten()?.forEach { user ->
Text(user.name)
}
Button(onClick = { setSize(size + 1) }) {
Text("Load More")
}
}Pagination-specific options can be set in the config block:
| Option name | Default value | Description |
|---|---|---|
| initialSize | 1 | Number of pages to load initially |
| revalidateAll | false | Revalidate all loaded pages |
| revalidateFirstPage | true | Revalidate the first page on changes |
| persistSize | false | Persist the page count across re-mounts |
rememberSWRImmutable() fetches data once and never revalidates automatically. Useful for data that doesn't change (e.g., static resources, user settings loaded once).
@Composable
fun StaticContent() {
val (data, error) = rememberSWRImmutable("/api/config", fetcher)
// ...
}This is equivalent to rememberSWR() with all revalidation options disabled.
You can provide a Persister to save fetched data to local storage (e.g., database, file system). This allows data to survive app restarts.
val persister = Persister<String, User>(
loadData = { key -> database.getUser(key) },
saveData = { key, data -> database.saveUser(key, data) },
)
@Composable
fun Profile() {
val (data, error) = rememberSWR("/api/user", fetcher, persister)
// On first load, persister.loadData is called for immediate display,
// then fetcher runs in background to revalidate.
}Deduplication and Deep Comparison are supported, but Dependency Collection is not supported due to Kotlin's language specification.
Therefore, the number of re-rendering (re-compose) may be higher than in the original React SWR.
However, it is possible to prevent performance degradation by limiting the number of arguments passed to child Composable functions (e.g., only data).
This library is inspired by React SWR ported for Jetpack Compose and Compose Multiplatform.
Supported platforms are Android, Desktop, iOS and Web.
The API specification of React SWR is followed as much as possible.
Options are also supported for the most part.
According to React SWR, "SWR" refers to
The name “SWR” is derived from stale-while-revalidate, a HTTP cache invalidation strategy popularized by HTTP RFC 5861. SWR is a strategy to first return the data from cache (stale), then send the fetch request (revalidate), and finally come with the up-to-date data.
Add the following gradle dependency exchanging *.*.* for the latest release.
implementation("com.kazakago.swr:swr-compose:*.*.*")As with React SWR, implement the "fetcher function" and set it with the key to rememberSWR().
Using Kotlin's Destructuring declarations for return values can be written in the same way as in React SWR.
private val fetcher: suspend (key: String) -> String = {
getNameApi.execute(key)
}
@Composable
fun Profile() {
val (data, error) = rememberSWR("/api/user", fetcher)
if (error != null) {
Text("failed to load")
} else if (data == null) {
Text("loading...")
} else {
Text("hello $data!")
}
}Live demo of Kotlin/Wasm is here.
Refer to the example module for details. This module works as a Compose Multiplatform app.
| Feature name | Note |
|---|---|
| Global Configuration | |
| Data Fetching | |
| Error Handling | |
| Auto Revalidation | |
| Conditional Data Fetching | |
| Arguments | |
| Mutation | Available by rememberSWRMutation()
|
| Pagination | Available by rememberSWRInfinite()
|
| Prefetching Data | Available by rememberSWRPreload()
|
| Subscription | Available by rememberSWRSubscription()
|
The following options are supported for React SWR.
https://swr.vercel.app/docs/options
| Option name | Default value | Description |
|---|---|---|
| revalidateIfStale | true | Revalidate when cached data is stale |
| revalidateOnMount | true | Revalidate when component mounts |
| revalidateOnFocus | true | Revalidate when app regains focus |
| revalidateOnReconnect | true | Revalidate when network reconnects |
| refreshInterval | 0.seconds | Polling interval for periodic refresh (0=disabled) |
| refreshWhenHidden | false | Continue polling when app is in background |
| refreshWhenOffline | false | Continue polling when offline |
| shouldRetryOnError | true | Retry fetching on error |
| dedupingInterval | 2.seconds | Time window to deduplicate identical requests |
| focusThrottleInterval | 5.seconds | Min interval between focus-triggered revalidations |
| loadingTimeout | 3.seconds | Timeout before onLoadingSlow callback fires |
| errorRetryInterval | 5.seconds | Wait time between error retries |
| errorRetryCount | Maximum number of error retries | |
| fallbackData | Data to display while loading | |
| onLoadingSlow | Callback when loading exceeds loadingTimeout
|
|
| onSuccess | Callback on successful data fetch | |
| onError | Callback on fetch error | |
| keepPreviousData | false | Keep previous data when key changes |
| isPaused | Pause all revalidation when returns true | |
| onErrorRetry | Exponential backoff | Custom error retry handler |
rememberSWRMutation() provides a way to mutate remote data. Unlike rememberSWR(), it does not automatically fetch data — mutations are triggered manually.
private val fetcher: suspend (key: String, arg: String) -> String = { key, arg ->
updateNameApi.execute(key, arg)
}
@Composable
fun UpdateProfile() {
val (trigger, isMutating) = rememberSWRMutation("/api/user", fetcher)
Button(
onClick = { scope.launch { trigger("new name") } },
enabled = !isMutating,
) {
Text(if (isMutating) "Updating..." else "Update")
}
}| Option name | Default value | Description |
|---|---|---|
| optimisticData | Data to display immediately before mutation resolves | |
| revalidate | false | Revalidate cached data after mutation |
| populateCache | false | Update cache with mutation result |
| rollbackOnError | true | Revert to previous data if mutation fails |
| onSuccess | Callback on successful mutation | |
| onError | Callback on mutation error |
rememberSWRSubscription() integrates with real-time data sources such as WebSockets, Server-Sent Events, or Firestore. Data received via the subscription is written to the SWR cache, so any rememberSWR() using the same key automatically displays the latest data.
@Composable
fun LiveFeed() {
val (data, error) = rememberSWRSubscription(
key = "/ws/feed",
subscribe = { key ->
callbackFlow {
val ws = WebSocket(key) { message ->
trySend(message)
}
awaitClose { ws.close() }
}
},
)
if (error != null) {
Text("error: ${error.message}")
} else {
Text("latest: ${data ?: "..."}")
}
}rememberSWRInfinite() provides paginated data fetching with dynamic page management.
@Composable
fun UserList() {
val (data, error, isValidating, isLoading, _, size, setSize) = rememberSWRInfinite(
getKey = { pageIndex, previousPageData ->
if (previousPageData != null && previousPageData.isEmpty()) null // reached the end
else "/api/users?page=$pageIndex"
},
fetcher = { key -> fetchUsers(key) },
)
// data is List<List<User>?> — one entry per page
data?.flatten()?.forEach { user ->
Text(user.name)
}
Button(onClick = { setSize(size + 1) }) {
Text("Load More")
}
}Pagination-specific options can be set in the config block:
| Option name | Default value | Description |
|---|---|---|
| initialSize | 1 | Number of pages to load initially |
| revalidateAll | false | Revalidate all loaded pages |
| revalidateFirstPage | true | Revalidate the first page on changes |
| persistSize | false | Persist the page count across re-mounts |
rememberSWRImmutable() fetches data once and never revalidates automatically. Useful for data that doesn't change (e.g., static resources, user settings loaded once).
@Composable
fun StaticContent() {
val (data, error) = rememberSWRImmutable("/api/config", fetcher)
// ...
}This is equivalent to rememberSWR() with all revalidation options disabled.
You can provide a Persister to save fetched data to local storage (e.g., database, file system). This allows data to survive app restarts.
val persister = Persister<String, User>(
loadData = { key -> database.getUser(key) },
saveData = { key, data -> database.saveUser(key, data) },
)
@Composable
fun Profile() {
val (data, error) = rememberSWR("/api/user", fetcher, persister)
// On first load, persister.loadData is called for immediate display,
// then fetcher runs in background to revalidate.
}Deduplication and Deep Comparison are supported, but Dependency Collection is not supported due to Kotlin's language specification.
Therefore, the number of re-rendering (re-compose) may be higher than in the original React SWR.
However, it is possible to prevent performance degradation by limiting the number of arguments passed to child Composable functions (e.g., only data).