{"id":29829873,"url":"https://github.com/chrynan/cycle","last_synced_at":"2025-07-29T09:41:39.273Z","repository":{"id":57154957,"uuid":"294552765","full_name":"chRyNaN/cycle","owner":"chRyNaN","description":"Kotlin Multi-platform Presentation Layer Design Pattern Library","archived":false,"fork":false,"pushed_at":"2025-07-07T07:20:34.000Z","size":1216,"stargazers_count":8,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-07-07T08:34:17.065Z","etag":null,"topics":["architecture","architecture-components","design-patterns","kotlin","kotlin-coroutines","kotlin-multi-platform","kotlin-multiplatform","kotlin-multiplatform-library","mvi","mvi-android","mvi-architecture","mvi-kotlin","mvvm","mvvm-android","mvvm-architecture","presentation","ui","viewmodel","viewmodels"],"latest_commit_sha":null,"homepage":"","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/chRyNaN.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/funding.yml","license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":"CITATION.cff","codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null},"funding":{"custom":["https://www.buymeacoffee.com/chrynan","https://chrynan.codes"]}},"created_at":"2020-09-11T00:24:32.000Z","updated_at":"2024-05-23T19:43:00.000Z","dependencies_parsed_at":"2023-09-25T09:51:38.093Z","dependency_job_id":"32ca5b7d-729d-45d2-8f79-f664f78026a0","html_url":"https://github.com/chRyNaN/cycle","commit_stats":null,"previous_names":[],"tags_count":22,"template":false,"template_full_name":null,"purl":"pkg:github/chRyNaN/cycle","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chRyNaN%2Fcycle","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chRyNaN%2Fcycle/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chRyNaN%2Fcycle/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chRyNaN%2Fcycle/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/chRyNaN","download_url":"https://codeload.github.com/chRyNaN/cycle/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chRyNaN%2Fcycle/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":267665373,"owners_count":24124522,"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","status":"online","status_checked_at":"2025-07-29T02:00:12.549Z","response_time":2574,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":["architecture","architecture-components","design-patterns","kotlin","kotlin-coroutines","kotlin-multi-platform","kotlin-multiplatform","kotlin-multiplatform-library","mvi","mvi-android","mvi-architecture","mvi-kotlin","mvvm","mvvm-android","mvvm-architecture","presentation","ui","viewmodel","viewmodels"],"created_at":"2025-07-29T09:41:26.842Z","updated_at":"2025-07-29T09:41:39.264Z","avatar_url":"https://github.com/chRyNaN.png","language":"Kotlin","readme":"![presentation](assets/branding_image.png)\n\n# cycle\n\nA Kotlin multi-platform presentation layer design pattern. This is a cyclic (hence the name) uni-directional data flow\n(UDF) design pattern library that is closely related to the MVI (Model-View-Intent) pattern on Android. It utilizes\nkotlinx.coroutines Flows and is easily compatible with modern UI Frameworks, such as Jetpack Compose.\n\n### Perform \u003e Reduce \u003e Compose\n\nThis design pattern breaks down complex application logic into three simple parts: **Perform** the actions, **Reduce**\nthe changes, and **Compose** the view from the state. This simple approach to application design is easy to reason\nabout, implement, debug, and test, and very flexible to adapt to any application's specific needs.\n\n\u003cimg alt=\"GitHub tag (latest by date)\" src=\"https://img.shields.io/github/v/tag/chRyNaN/presentation\"\u003e\n\n```kotlin\nfun counterReducer(state: Int?, change: CounterChange): Int {\n    val value = state ?: 0\n\n    return when (change) {\n        CounterChange.INCREMENT -\u003e value + 1\n        CounterChange.DECREMENT -\u003e value - 1\n    }\n}\n\n@Composable\nfun Counter() {\n    val viewModel = remember { ViewModel.create(reducer = ::counterReducer) }\n\n    val state by viewModel.stateChanges()\n\n    Text(\"Count = $state\")\n\n    LaunchedEffect(Unit) {\n        viewModel.dispatch(CounterChange.INCREMENT) // 1\n        viewModel.dispatch(CounterChange.INCREMENT) // 2\n        viewModel.dispatch(CounterChange.DECREMENT) // 1\n    }\n}\n```\n\n## Getting Started 🏁\n\nThe library is provided through [Repsy.io](https://repsy.io/). Checkout the\n[releases page](https://github.com/chRyNaN/presentation/releases) to get the latest version. \u003cbr/\u003e\u003cbr/\u003e\n\u003cimg alt=\"GitHub tag (latest by date)\" src=\"https://img.shields.io/github/v/tag/chRyNaN/presentation\"\u003e\n\n### Repository\n\n```kotlin\nrepositories {\n    maven {\n        url = uri(\"https://repo.repsy.io/mvn/chrynan/public\")\n    }\n}\n```\n\n### Dependencies\n\n#### core\n\n```kotlin\nimplementation(\"com.chrynan.cycle:cycle-core:$VERSION\")\n```\n\n#### compose\n\n```kotlin\nimplementation(\"com.chrynan.cycle:cycle-compose:$VERSION\")\n```\n\n## Usage 👨‍💻\n\n### State Management (Perform and Reduce)\n\nThe first two parts of a cycle are **Perform** and **Reduce** which, together, invoke application logic that produces\nchanges, which then get reduced to create a new state. This process, which is illustrated below, can ultimately be\nconsidered as state management since it involves the creation, alteration, and storage of state.\n\n#### [Redux Counter Example](https://redux.js.org/introduction/getting-started#basic-example)\n\nThe following is an example of using a `StateStore` component from this library to implement the same example shown in\nthe [Redux Javascript Library's Documentation](https://redux.js.org/introduction/getting-started#basic-example), but in\nKotlin.\n\n```kotlin\nenum class CounterChange {\n\n    INCREMENT,\n    DECREMENT\n}\n\nfun counterReducer(state: Int?, change: CounterChange): Int {\n    val value = state ?: 0\n\n    return when (change) {\n        CounterChange.INCREMENT -\u003e value + 1\n        CounterChange.DECREMENT -\u003e value - 1\n    }\n}\n\nfun testCounter(coroutineScope: CoroutineScope) {\n    val store = MutableStateStore(reducer = ::counterReducer)\n\n    store.subscribe(coroutineScope = coroutineScope) { state -\u003e\n        println(state)\n    }\n\n    coroutineScope.launch {\n        store.dispatch(CounterChange.INCREMENT) // 1\n        store.dispatch(CounterChange.INCREMENT) // 2\n        store.dispatch(CounterChange.DECREMENT) // 1\n    }\n}\n```\n\nThe above example is a good simple demonstration, but it isn't very useful for more complex, \"real-world\" applications.\nWhile the fundamentals are the same, applications often require a more complex flow of logic. Coordinating the flow of\nlogic efficiently between different application components is the responsibility of a design pattern.\n\nThere are many application level design patterns (MVC, MVP, MVVM, MVI, to name a few), but this library focuses on MVVM\nand MVI design patterns, since those are easily reactive (using Kotlin Coroutine Flows) and easily supportive of the\nUDF (uni-directional data flow) design principal. There is a `ViewModel` component provided by this library which can\nencapsulate component specific functionality. The above example can be updated to utilize a `ViewModel` and perform\nmore complex actions at the call-site:\n\n```kotlin\nfun testCounter() {\n    val viewModel = ViewModel.create(reducer = ::counterReducer).apply { bind() }\n\n    viewModel.subscribe { state -\u003e\n        println(state)\n    }\n\n    viewModel.dispatch(CounterChange.INCREMENT) // 1\n    viewModel.dispatch(CounterChange.INCREMENT) // 2\n    viewModel.dispatch(CounterChange.DECREMENT) // 1\n\n    // The provided action will be invoked and must return a Flow of changes\n    // 2\n    viewModel.perform {\n        flow {\n            emit(CounterChange.INCREMENT)\n\n            if ((viewModel.currentState ?: 0) \u003e 2) {\n                emit(CounterChange.DECREMENT)\n            }\n        }\n    }\n\n    viewModel.unbind()\n}\n```\n\nThe above example illustrates the usage of the `ViewModel.perform` function, which takes an `Action` value as a\nparameter. An `Action` is simply a `typealias` for a suspending function that takes the current `State` as a parameter\nand returns a `Flow` of `Changes`. This function is typically not invoked at the call-site, as in the example above,\nbut instead invoked by `ViewModel` implementing classes. This forces the logic to be well-defined, encapsulated within\na single component, and easily testable. The above example re-written to use a custom `ViewModel` might look like the\nfollowing:\n\n```kotlin\nclass CounterViewModel : ViewModel\u003cInt, CounterChange\u003e(\n    stateStore = MutableStateStore(reducer = ::counterReducer)\n) {\n\n    fun increment() = dispatch(CounterChange.INCREMENT)\n\n    fun decrement() = dispatch(CounterChange.DECREMENT)\n\n    fun incrementIfLessThanTwo() = perform {\n        flow {\n            emit(CounterChange.INCREMENT)\n\n            if ((currentState ?: 0) \u003e 2) {\n                emit(CounterChange.DECREMENT)\n            }\n        }\n    }\n}\n\nfun testCounter() {\n    val viewModel = CounterViewModel().apply { bind() }\n\n    viewModel.subscribe { state -\u003e\n        println(state)\n    }\n\n    // Note: The dispatch function is no longer public, so we can't access it here.\n    viewModel.increment() // 1\n    viewModel.increment() // 2\n    viewModel.decrement() // 1\n\n    // Note: The perform function is no longer public, so we can't access it here.\n    viewModel.incrementIfLessThanTwo() // 2\n\n    viewModel.unbind()\n}\n```\n\nAnother common design pattern is MVI (Model-View-Intent). With this design pattern, an `Intent` model is emitted on the\n`ViewModel's` reactive stream, which triggers an associated `Action`, resulting in a `Flow` of `Changes` being emitted\nand reduced to produce new `States`. This is similar to the above example, but instead of having separate functions on\nthe `ViewModel` for each action, we will have a single `intent(to:)` function on the `ViewModel` that takes an `Intent`\nmodel and performs the appropriate action based on that value. This approach can easily be implemented with this\nlibrary by extending the `IntentViewModel` class:\n\n```kotlin\nenum class CounterIntent {\n\n    INCREMENT,\n    DECREMENT,\n    INCREMENT_IF_LESS_THAN_TWO\n}\n\nenum class CounterChange {\n\n    INCREMENTED,\n    DECREMENTED,\n    NO_CHANGE\n}\n\nfun counterReducer(state: Int?, change: CounterChange): Int {\n    val value = state ?: 0\n\n    return when (change) {\n        CounterChange.INCREMENTED -\u003e value + 1\n        CounterChange.DECREMENTED -\u003e value - 1\n        CounterChange.NO_CHANGE -\u003e value\n    }\n}\n\nclass CounterViewModel : IntentViewModel\u003cCounterIntent, Int, CounterChange\u003e(\n    stateStore = MutableStateStore(reducer = ::counterReducer)\n) {\n\n    override fun performIntentAction(state: Int?, intent: CounterIntent): Flow\u003cCounterChange\u003e = flow {\n        val change = when (intent) {\n            CounterIntent.INCREMENT -\u003e CounterChange.INCREMENTED\n            CounterIntent.INCREMENT_IF_LESS_THAN_TWO -\u003e CounterChange.NO_CHANGE\n            CounterIntent.DECREMENT -\u003e CounterChange.DECREMENTED\n        }\n\n        emit(change)\n    }\n}\n\nfun testCounter() {\n    val viewModel = CounterViewModel().apply { bind() }\n\n    viewModel.subscribe { state -\u003e\n        println(state)\n    }\n\n    // Note: The dispatch function is no longer public, so we can't access it here.\n    viewModel.intent(to = CounterIntent.INCREMENT) // 1\n    viewModel.intent(to = CounterIntent.INCREMENT) // 2\n    viewModel.intent(to = CounterIntent.DECREMENT) // 1\n\n    // Note: The perform function is no longer public, so we can't access it here.\n    viewModel.intent(to = CounterIntent.INCREMENT_IF_LESS_THAN_TWO)\n\n    viewModel.unbind()\n}\n```\n\n### UI Management (Compose)\n\nThe third and final part of a cycle is **Compose** which is responsible for listening to new states and updating a UI\nview accordingly. This part's implementation is dependent on the UI framework used, but can easily be adapted to fit\nmost modern UI frameworks.\n\nThe easiest way to subscribe to state changes to update the UI, is to use the `subscribe` function:\n\n```kotlin\nviewModel.subscribe { state -\u003e\n    // Update the UI or trigger a UI refresh here using the new state.\n}\n```\n\n**Note:** That a `ViewModel` has a lifecycle which is defined by the invocation of its `bind/unbind` functions.\nTherefore, the `ViewModel.bind` function must be called before the `ViewModel.subscribe` function is invoked, otherwise\nno states will be emitted to the `subscribe` function closure.\n\nAlternatively, you can use the [cycle-compose](#dependencies) dependency when targeting Jetpack Compose for a simple\nintegration. Use the `stateChanges()` to convert the `Flow` of `State` changes to a Jetpack Compose `State`. This\napproach also handles binding and unbinding of the `ViewModel` for you.\n\n```kotlin\n@Composable\nfun Home(viewModel: HomeViewModel) {\n    val state by viewModel.stateChanges()\n\n    // Use the state to construct the UI.\n}\n```\n\nIn the example above, the `stateChanges` function binds the `ViewModel` to the lifecycle of the composable function and\nlistens to changes in the `State`. The type is converted from a `Flow` of `States` to a Jetpack Compose `State`, so when\na state change occurs, it triggers recomposition of the composable function.\n\n#### View\n\nThe `View` interface represents a UI component that contains a `ViewModel` and properly binds its lifecycle to that of\nthe UI component. This interface can be used to encapsulate lifecycle and logic within the framework defined UI\ncomponent implementation.\n\n## Documentation 📃\n\nMore detailed documentation is available in the [docs](docs/) folder. The entry point to the documentation can be\nfound [here](docs/index.md).\n\n## Security 🛡️\n\nFor security vulnerabilities, concerns, or issues, please responsibly disclose the information either by opening a\npublic GitHub Issue or reaching out to the project owner.\n\n## Contributing ✍️\n\nOutside contributions are welcome for this project. Please follow the [code of conduct](CODE_OF_CONDUCT.md)\nand [coding conventions](CODING_CONVENTIONS.md) when contributing. If contributing code, please add thorough documents.\nand tests. Thank you!\n\n## Sponsorship ❤️\n\nSupport this project by [becoming a sponsor](https://www.buymeacoffee.com/chrynan) of my work! And make sure to give the\nrepository a ⭐\n\n## License ⚖️\n\n```\nCopyright 2021 chRyNaN\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://www.buymeacoffee.com/chrynan","https://chrynan.codes"],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchrynan%2Fcycle","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fchrynan%2Fcycle","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchrynan%2Fcycle/lists"}