
Automatic retries, loading-state management, configurable caching and pagination with reactive flows, Compose-friendly UI helpers and ViewModel integration to simplify resilient, low-boilerplate data fetching.
A Kotlin Multiplatform library that simplifies data fetching with automatic retry logic, loading state management, and flexible caching strategies. It is designed to integrate seamlessly with Jetbrains/Jetpack Compose for an offline-first experience.
In your ViewModel, use the reflow extension function to wrap your data fetching logic:
class MyViewModel : ViewModel() {
val dataFlow = reflow {
api.fetchData()
}
}Use ReflowContent to automatically handle loading, error, and success states. It does also work on Compose Multiplatform
@Composable
fun MyScreen(viewModel: MyViewModel) {
ReflowContent(viewModel.dataFlow) { data ->
MyContent(data)
}
}or
@Composable
fun MyScreen(viewModel: MyViewModel) {
val dataFlow = viewModel.dataFlow
PullToRefreshBox(
isRefreshing = dataFlow.state.isLoading,
onRefresh = { dataFlow.refresh() },
) {
ReflowContent(dataFlow) { data ->
MyContent(data)
}
}
}val data = reflow(
cacheSource = CacheSource.None(), // default
dispatcher = Dispatchers.IO, // default
initial = Resulting.loading(), // default
shouldLoadingOnRefresh = true, // default
maxRetries = 3, // default value, 0 to disable
retryDelay = 2_000L, // default value
shouldRetry = { it is HttpException },
) {
api.fetchData()
}You can also use the state property directly and extracting the fetch state:
@Composable
fun MyScreen(viewModel: MyViewModel) {
val dataState by viewModel.ui.state
dataState.foldUi(
onLoading = { CircularProgressIndicator() },
onSuccess = { value -> MyContent(value) },
onFailure = { error -> ErrorMessage(onRetry = { viewModel.uiState.refresh() }) }
)
}Add the dependency to your module build.gradle.kts
dependencies {
implementation("io.github.araujojordan:reflow:0.3.0") // Add this
}For KMP, just add it on commonMain.dependencies {}
Reflow supports multiple caching strategies through the CacheSource parameter:
| Strategy | Description | Requirement |
|---|---|---|
CacheSource.None() |
No caching (Default) | None |
CacheSource.Memory(key) |
In-memory LRU cache. | A unique String key (optional) |
CacheSource.Disk(key) |
Persistent disk cache using DataStore. | A unique String key (optional) and the class must be @Serializable
|
Note: If you plan to use
CacheSource.Disk(), you must also apply the@Serializablefrom Kotlin Serialization in the class that you want to cache. If no key is provided onMemoryorDisk, it will useT::class.qualifiedNameas key and show a warning. Not using an unique key will result in wrong behavior when caching/retrieving the same types of objects in different places.
Example with Disk caching:
@Serializable
data class User(val id: Int, val name: String)
val userName = reflow(Disk("user_data")) {
api.fetchUser()
}Reflow provides a streamlined way to handle paginated data:
class MyViewModel : ViewModel() {
val list = reflowPaginated { page ->
api.fetchList(page = page.number, size = page.pageSize)
}
}Use the built-in LazyColumnPaginated for automatic pagination with loading states:
@Composable
fun UserListScreen(viewModel: MyViewModel) {
LazyColumnPaginated(
paginatedFlow = viewModel.users,
modifier = Modifier.fillMaxSize()
) { user ->
UserItem(user = user)
}
}For tasks like a RestFull Post or a GraphQL Mutation you can use rexecute:
class MyViewModel : ViewModel() {
val toggleState = reflow(Disk("user_profile_notification")) {
api.getUserNotificationState() // Returns a Boolean
}
fun onToggleChange(toggle: Boolean) = rexecute(key = "user_profile_notification") {
api.toggleUserNotification(toggle) // Returns a Boolean
}
}Note: Passing the same key will automatically reuse the
api.toggleUserNotification()task result ontoggleState. This will work on different screens/VMs without the need to pass the result manually.
reflow reusal.Flow<Resulting<T>> that can be easily observed in your UI for loading and error handling.This project is licensed under the MIT License - see the LICENSE file for details.
Made with ❤️ by AraujoJordan
A Kotlin Multiplatform library that simplifies data fetching with automatic retry logic, loading state management, and flexible caching strategies. It is designed to integrate seamlessly with Jetbrains/Jetpack Compose for an offline-first experience.
In your ViewModel, use the reflow extension function to wrap your data fetching logic:
class MyViewModel : ViewModel() {
val dataFlow = reflow {
api.fetchData()
}
}Use ReflowContent to automatically handle loading, error, and success states. It does also work on Compose Multiplatform
@Composable
fun MyScreen(viewModel: MyViewModel) {
ReflowContent(viewModel.dataFlow) { data ->
MyContent(data)
}
}or
@Composable
fun MyScreen(viewModel: MyViewModel) {
val dataFlow = viewModel.dataFlow
PullToRefreshBox(
isRefreshing = dataFlow.state.isLoading,
onRefresh = { dataFlow.refresh() },
) {
ReflowContent(dataFlow) { data ->
MyContent(data)
}
}
}val data = reflow(
cacheSource = CacheSource.None(), // default
dispatcher = Dispatchers.IO, // default
initial = Resulting.loading(), // default
shouldLoadingOnRefresh = true, // default
maxRetries = 3, // default value, 0 to disable
retryDelay = 2_000L, // default value
shouldRetry = { it is HttpException },
) {
api.fetchData()
}You can also use the state property directly and extracting the fetch state:
@Composable
fun MyScreen(viewModel: MyViewModel) {
val dataState by viewModel.ui.state
dataState.foldUi(
onLoading = { CircularProgressIndicator() },
onSuccess = { value -> MyContent(value) },
onFailure = { error -> ErrorMessage(onRetry = { viewModel.uiState.refresh() }) }
)
}Add the dependency to your module build.gradle.kts
dependencies {
implementation("io.github.araujojordan:reflow:0.3.0") // Add this
}For KMP, just add it on commonMain.dependencies {}
Reflow supports multiple caching strategies through the CacheSource parameter:
| Strategy | Description | Requirement |
|---|---|---|
CacheSource.None() |
No caching (Default) | None |
CacheSource.Memory(key) |
In-memory LRU cache. | A unique String key (optional) |
CacheSource.Disk(key) |
Persistent disk cache using DataStore. | A unique String key (optional) and the class must be @Serializable
|
Note: If you plan to use
CacheSource.Disk(), you must also apply the@Serializablefrom Kotlin Serialization in the class that you want to cache. If no key is provided onMemoryorDisk, it will useT::class.qualifiedNameas key and show a warning. Not using an unique key will result in wrong behavior when caching/retrieving the same types of objects in different places.
Example with Disk caching:
@Serializable
data class User(val id: Int, val name: String)
val userName = reflow(Disk("user_data")) {
api.fetchUser()
}Reflow provides a streamlined way to handle paginated data:
class MyViewModel : ViewModel() {
val list = reflowPaginated { page ->
api.fetchList(page = page.number, size = page.pageSize)
}
}Use the built-in LazyColumnPaginated for automatic pagination with loading states:
@Composable
fun UserListScreen(viewModel: MyViewModel) {
LazyColumnPaginated(
paginatedFlow = viewModel.users,
modifier = Modifier.fillMaxSize()
) { user ->
UserItem(user = user)
}
}For tasks like a RestFull Post or a GraphQL Mutation you can use rexecute:
class MyViewModel : ViewModel() {
val toggleState = reflow(Disk("user_profile_notification")) {
api.getUserNotificationState() // Returns a Boolean
}
fun onToggleChange(toggle: Boolean) = rexecute(key = "user_profile_notification") {
api.toggleUserNotification(toggle) // Returns a Boolean
}
}Note: Passing the same key will automatically reuse the
api.toggleUserNotification()task result ontoggleState. This will work on different screens/VMs without the need to pass the result manually.
reflow reusal.Flow<Resulting<T>> that can be easily observed in your UI for loading and error handling.This project is licensed under the MIT License - see the LICENSE file for details.
Made with ❤️ by AraujoJordan