{"id":28067661,"url":"https://github.com/skydoves/flow-operators","last_synced_at":"2025-05-12T16:59:05.811Z","repository":{"id":274269441,"uuid":"922414521","full_name":"skydoves/flow-operators","owner":"skydoves","description":"🌊 Flow operators enable you to create restartable, pausable, or one-shot StateFlow.","archived":false,"fork":false,"pushed_at":"2025-02-07T11:05:34.000Z","size":149,"stargazers_count":120,"open_issues_count":1,"forks_count":4,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-02-07T12:22:08.964Z","etag":null,"topics":["android","coroutines","flow","kotlin","skydoves","stateflow"],"latest_commit_sha":null,"homepage":"https://proandroiddev.com/loading-initial-data-in-launchedeffect-vs-viewmodel-f1747c20ce62","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/skydoves.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":"skydoves","custom":["https://www.paypal.me/skydoves","https://www.buymeacoffee.com/skydoves"]}},"created_at":"2025-01-26T06:08:12.000Z","updated_at":"2025-02-07T11:19:39.000Z","dependencies_parsed_at":"2025-01-26T07:18:40.396Z","dependency_job_id":"27302195-7730-491a-bea4-ce6b2069049b","html_url":"https://github.com/skydoves/flow-operators","commit_stats":null,"previous_names":["skydoves/flow-operators"],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skydoves%2Fflow-operators","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skydoves%2Fflow-operators/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skydoves%2Fflow-operators/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skydoves%2Fflow-operators/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/skydoves","download_url":"https://codeload.github.com/skydoves/flow-operators/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253784765,"owners_count":21963896,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","coroutines","flow","kotlin","skydoves","stateflow"],"created_at":"2025-05-12T16:59:05.279Z","updated_at":"2025-05-12T16:59:05.805Z","avatar_url":"https://github.com/skydoves.png","language":"Kotlin","readme":"\u003ch1 align=\"center\"\u003eFlow Operators\u003c/h1\u003e\u003c/br\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://opensource.org/licenses/Apache-2.0\"\u003e\u003cimg alt=\"License\" src=\"https://img.shields.io/badge/License-Apache%202.0-blue.svg\"/\u003e\u003c/a\u003e\n  \u003ca href=\"https://android-arsenal.com/api?level=21\"\u003e\u003cimg alt=\"API\" src=\"https://img.shields.io/badge/API-21%2B-brightgreen.svg?style=flat\"/\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/skydoves/flow-operators/actions/workflows/android.yml\"\u003e\u003cimg alt=\"Build Status\" \n  src=\"https://github.com/skydoves/flow-operators/actions/workflows/android.yml/badge.svg\"/\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/skydoves\"\u003e\u003cimg alt=\"Profile\" src=\"https://skydoves.github.io/badges/skydoves.svg\"/\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/doveletter\"\u003e\u003cimg alt=\"Profile\" src=\"https://skydoves.github.io/badges/dove-letter.svg\"/\u003e\u003c/a\u003e\n\u003c/p\u003e\u003cbr\u003e\n\n\u003cp align=\"center\"\u003e🌊 Flow operators enable you to create restartable, pausable, or one-shot StateFlow, and they support KMP. \u003c/p\u003e\n\n## Flow Operators\n\nFlow operators that enable you to create restartable, pausable, or one-shot `StateFlow` instances, allowing you to customize and control additional behaviors for `StateFlow` based on your specific use case. Why are flow operators useful? You can manage state and control data streams efficiently in complex scenarios. You can read more about the reasons in [Loading Initial Data on Android Part 2: Clear All Your Doubts](https://medium.com/proandroiddev/loading-initial-data-part-2-clear-all-your-doubts-0f621bfd06a0).\n\n[![Maven Central](https://img.shields.io/maven-central/v/com.github.skydoves/flow-operators.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22com.github.skydoves%22%20AND%20a:%22flow-operators%22)\n\n### Version Catalog\n\nIf you're using Version Catalog, you can configure the dependency by adding it to your `libs.versions.toml` file as follows:\n\n```toml\n[versions]\n#...\nflowOperators = \"0.1.1\"\n\n[libraries]\n#...\nflow-operators = { module = \"com.github.skydoves:flow-operators\", version.ref = \"flowOperators\" }\n```\n\n### Gradle\nAdd the dependency below to your **module**'s `build.gradle.kts` file:\n\n```gradle\ndependencies {\n    implementation(\"com.github.skydoves:flow-operators:$version\")\n    \n    // if you're using Version Catalog\n    implementation(libs.flow.operators)\n}\n```\n\nFor Kotlin Multiplatform, add the dependency below to your **module**'s `build.gradle.kts` file:\n\n```gradle\nsourceSets {\n    val commonMain by getting {\n        dependencies {\n            implementation(\"com.github.skydoves:flow-operators:$version\")\n        }\n    }\n}\n```\n\n### RestartableStateFlow\n\n`RestartableStateFlow` extends both `StateFlow` and `Restartable`, allowing the upstream flow to restart its emission. It behaves like a regular `StateFlow` but includes the ability to reset and restart the emission process when necessary.\n\nConsider a scenario where you load initial data using a property, as shown in the example below, instead of initiating the load inside `LaunchedEffect` or `ViewModel.init()`. (For more details, check out [Loading Initial Data in LaunchedEffect vs. ViewModel](https://medium.com/proandroiddev/loading-initial-data-in-launchedeffect-vs-viewmodel-f1747c20ce62)).\n\n```kotlin\nval posters: StateFlow\u003cList\u003cPoster\u003e\u003e = mainRepository.fetchPostersFlow()\n.filter { it.isSuccess }\n.mapLatest { it.getOrThrow() }\n.restartableStateIn(\n    scope = viewModelScope,\n    started = SharingStarted.WhileSubscribed(5000),\n    initialValue = emptyList(),\n)\n```\n\nBy using a property, the data is loaded only when the first subscription occurs, preventing the unnecessary immediate execution of tasks and avoiding unintended side effects that might arise from `ViewModel.init()` or `LaunchedEffect`. However, another challenge arises: you may need to reload the data due to scenarios like refreshing, recovering from errors during the initial load, or other reasons. In such cases, you can seamlessly restart the upstream flow using `RestartableStateFlow`.\n\n```kotlin\nclass MainViewModel(mainRepository: MainRepository) : ViewModel() {\n\n  private val restartablePoster: RestartableStateFlow\u003cList\u003cPoster\u003e\u003e = mainRepository.fetchPostersFlow()\n    .filter { it.isSuccess }\n    .mapLatest { it.getOrThrow() }\n    .restartableStateIn(\n      scope = viewModelScope,\n      started = SharingStarted.WhileSubscribed(5000),\n      initialValue = emptyList(),\n    )\n\n  val posters: StateFlow\u003cList\u003cPoster\u003e\u003e = restartablePoster // don't expose the Restartable interface to the outside\n\n  fun refresh() = restartablePoster.restart()\n}\n```\n\nNow, you can easily restart the upstream flow and reload the initial task using the property.\n\n```kotlin\n@Composable\nprivate fun Main(mainViewModel: MainViewModel) {\n  val posters by mainViewModel.posters.collectAsStateWithLifecycle()\n\n  Column(\n    modifier = Modifier\n      .fillMaxSize()\n      .padding(6.dp)\n      .verticalScroll(rememberScrollState()),\n  ) {\n    Button(onClick = { mainViewModel.refresh() }) {\n      Text(text = \"restart\")\n    }\n\n    Text(text = posters.toString())\n  }\n}\n```\n\n### PausableStateFlow\n\n`PausableStateFlow` extends both `StateFlow` and `PausableStateFlow`, enabling the upstream flow to pause and resume its emission. It retains the functionality of a standard `StateFlow` while adding controls for pausing and resuming emissions as needed.\n\nThe core concept of `PausableStateFlow` is similar to `RestartableStateFlow`, but with an added capability: it allows you to pause and resume listening to the upstream flow. This can be particularly useful in scenarios where you want to temporarily stop processing updates from an upstream flow, such as real-time location updates, Bluetooth connection status, animations, or other continuous events. \n\nWhile paused, any new subscribers to the `PausableStateFlow` will simply receive the latest cached value instead of actively listening to the upstream emissions.\n\n```kotlin\nclass MainViewModel(mainRepository: MainRepository) : ViewModel() {\n  \n  private val pausableStateFlow: PausableStateFlow\u003cList\u003cPoster\u003e\u003e =\n    mainRepository.fetchPostersFlow()\n      .filter { it.isSuccess }\n      .mapLatest { it.getOrThrow() }\n      .pausableStateIn(\n        scope = viewModelScope,\n        started = SharingStarted.WhileSubscribed(5000),\n        initialValue = emptyList(),\n      )\n\n  val posters: StateFlow\u003cList\u003cPoster\u003e\u003e = pausableStateFlow\n\n  fun pause() = pausableStateFlow.pause()\n  \n  fun resume() = pausableStateFlow.resume()\n}\n```\n\n### OnetimeWhileSubscribed\n\n`OnetimeWhileSubscribed` is a `SharingStarted` strategy that ensures the upstream flow emits only once while a subscriber is active. After the initial emission, it remains idle until another active subscription appears.\n\nWhen converting a cold flow into a hot flow using `stateIn` on Android, a common approach is to use `SharingStarted.WhileSubscribed(5_000)` with the `stateIn` function. The 5-second threshold (5_000) aligns with the ANR (Application Not Responding) timeout limit. If no subscribers remain for longer than 5 seconds, the timeout is exceeded, and the upstream data flow ceases to influence your UI layer.\n\nHowever, this can lead to another side-effect: if you navigate from Screen A to Screen B and remain on Screen B for over 5 seconds, returning to Screen A will restart the upstream flow. This causes the same task to relaunch, even if it was already completed. To avoid this, you can use `OnetimeWhileSubscribed` as a `SharingStarted` strategy. It ensures the upstream flow is launched only once when the first subscription occurs, and subsequently, only the latest cached value is replayed, avoiding redundant task restarts during screen transitions.\n\n```kotlin\nclass MainViewModel(mainRepository: MainRepository) : ViewModel() {\n  \n  private val posters: StateFlow\u003cList\u003cPoster\u003e\u003e =\n    mainRepository.fetchPostersFlow()\n      .filter { it.isSuccess }\n      .mapLatest { it.getOrThrow() }\n      .stateIn(\n        scope = viewModelScope,\n        started = SharingStarted.OnetimeWhileSubscribed(5_000),\n        initialValue = emptyList(),\n      )\n}\n```\n\n## Find this repository useful? :heart:\nSupport it by joining __[stargazers](https://github.com/skydoves/flow-operators/stargazers)__ for this repository. :star: \u003cbr\u003e\nAlso, __[follow me](https://github.com/skydoves)__ on GitHub for my next creations! 🤩\n\n# License\n```xml\nDesigned and developed by 2025 skydoves (Jaewoong Eum)\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n   http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n```\n","funding_links":["https://github.com/sponsors/skydoves","https://www.paypal.me/skydoves","https://www.buymeacoffee.com/skydoves"],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fskydoves%2Fflow-operators","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fskydoves%2Fflow-operators","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fskydoves%2Fflow-operators/lists"}