{"id":19620588,"url":"https://github.com/tunjid/mutator","last_synced_at":"2025-04-28T03:32:07.576Z","repository":{"id":41805903,"uuid":"404729892","full_name":"tunjid/Mutator","owner":"tunjid","description":"Flow powered mutual exclusion for UI state mutation over time.","archived":false,"fork":false,"pushed_at":"2024-02-17T10:46:52.000Z","size":13915,"stargazers_count":50,"open_issues_count":0,"forks_count":9,"subscribers_count":2,"default_branch":"develop","last_synced_at":"2025-04-05T05:51:09.751Z","etag":null,"topics":["android","architecture","compose","coroutines","flow","jetpack-compose","kotlin","kotlin-library","multiplatform","mvi","mvi-android","mvi-coroutines-flow","mvvm","mvvm-android","reactive-programming","reactive-streams","redux","state-management","udf","unidirectional-data-flow"],"latest_commit_sha":null,"homepage":"https://tunjid.github.io/Mutator/","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/tunjid.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":"contributing.md","funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-09-09T13:20:00.000Z","updated_at":"2025-02-06T11:30:14.000Z","dependencies_parsed_at":"2024-11-11T11:19:39.236Z","dependency_job_id":"72f0a3fa-3867-462d-b1fe-a20f1d5ea573","html_url":"https://github.com/tunjid/Mutator","commit_stats":{"total_commits":204,"total_committers":5,"mean_commits":40.8,"dds":0.5,"last_synced_commit":"0c94b699a3324f69488caa3847c59c26e314b421"},"previous_names":[],"tags_count":27,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tunjid%2FMutator","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tunjid%2FMutator/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tunjid%2FMutator/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tunjid%2FMutator/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tunjid","download_url":"https://codeload.github.com/tunjid/Mutator/tar.gz/refs/heads/develop","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251246151,"owners_count":21558759,"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","architecture","compose","coroutines","flow","jetpack-compose","kotlin","kotlin-library","multiplatform","mvi","mvi-android","mvi-coroutines-flow","mvvm","mvvm-android","reactive-programming","reactive-streams","redux","state-management","udf","unidirectional-data-flow"],"created_at":"2024-11-11T11:19:20.857Z","updated_at":"2025-04-28T03:32:06.062Z","avatar_url":"https://github.com/tunjid.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Mutator\n\n[![JVM Tests](https://github.com/tunjid/Mutator/actions/workflows/tests.yml/badge.svg)](https://github.com/tunjid/Mutator/actions/workflows/tests.yml)\n![Mutator Core](https://img.shields.io/maven-central/v/com.tunjid.mutator/core?label=mutator-core)\n![Mutator Coroutines](https://img.shields.io/maven-central/v/com.tunjid.mutator/coroutines?label=mutator-coroutines)\n\n![badge][badge-ios]\n![badge][badge-js]\n![badge][badge-jvm]\n![badge][badge-linux]\n![badge][badge-windows]\n![badge][badge-mac]\n![badge][badge-tvos]\n![badge][badge-watchos]\n\n![Android Weekly Feature](https://androidweekly.net/issues/issue-510/badge)\n\nPlease note, this is not an official Google repository. It is a Kotlin multiplatform experiment\nthat makes no guarantees about API stability or long term support. None of the works presented here\nare production tested, and should not be taken as anything more than its face value.\n\n## Introduction\n\nMutator is a Kotlin multiplatform library that provides a suite of tools that help with producing state while following unidirectional data flow (UDF) principles. More specifically it provides implementations of the paradigm `newState = oldState + Δstate`.\n\nWhere `StateMutator`s are defined as:\n\n```kotlin\ninterface StateMutator\u003cState : Any\u003e {\n    val state: State\n}\n```\n\nand `Δstate` represents state changes over time and is expressed in Kotlin with the type:\n\n```kotlin\ntypealias Mutation\u003cState\u003e = State.() -\u003e State\n```\n\nAt the moment, there are two implementations:\n\n```kotlin\nfun \u003cState : Any\u003e CoroutineScope.stateFlowMutator(\n    initialState: State,\n    started: SharingStarted,\n    inputs: List\u003cFlow\u003cMutation\u003cState\u003e\u003e\u003e\n): StateMutator\u003cStateFlow\u003cState\u003e\u003e  \n```\n\nand \n\n```kotlin\nfun \u003cAction : Any, State : Any\u003e CoroutineScope.actionStateFlowMutator(\n    initialState: State,\n    started: SharingStarted,\n    inputs: List\u003cFlow\u003cMutation\u003cState\u003e\u003e\u003e,\n    actionTransform: (Flow\u003cAction\u003e) -\u003e Flow\u003cMutation\u003cState\u003e\u003e\n): StateMutator\u003cStateFlow\u003cState\u003e\u003e\n```\n\n`stateFlowMutator` is well suited for MVVM style applications and `actionStateFlowMutator` for MVI like approaches.\n\n## Foreground execution limits\n\nBoth implementations enforce that coroutines launched in them are only active as specified by the `SharingStarted`\npolicies passed to them. For most UI `StateMutator`s, this is typically `SharingStarted.whileSubscribed(duration)`.\nAny work launched that does not fit into this policy (a photo upload for example) should be queued to be run with the\nappropriate API on the platform you're working on. On Android, this is `WorkManager`.\n\n## Download\n\n```kotlin\nimplementation(\"com.tunjid.mutator:core:version\")\nimplementation(\"com.tunjid.mutator:coroutines:version\")\n```\n\nWhere the latest version is indicated by the badge at the top of this file.\n\n## Examples and sample code\n\nPlease refer to the project [website](https://tunjid.github.io/Mutator/) for an interactive walk through of the problem space this library operates in and visual examples.\n\n\n### `CoroutineScope.stateFlowMutator`\n\n`CoroutineScope.stateFlowMutator` returns a class that allows for mutating an initial state over time, by providing a `List` of `Flows` that contribute to state changes. A simple example follows:\n\n```kotlin\ndata class SnailState(\n    val progress: Float = 0f,\n    val speed: Speed = Speed.One,\n    val color: Color = Color.Blue,\n    val colors: List\u003cColor\u003e = MutedColors.colors(false).map(::Color)\n)\n\nclass SnailStateHolder(\n    scope: CoroutineScope\n) {\n\n    private val speed: Flow\u003cSpeed\u003e = scope.speedFlow()\n\n    private val speedChanges: Flow\u003cMutation\u003cSnail7State\u003e\u003e = speed\n        .map { mutationOf { copy(speed = it) } }\n\n    private val progressChanges: Flow\u003cMutation\u003cSnail7State\u003e\u003e = speed\n        .toInterval()\n        .map { mutationOf { copy(progress = (progress + 1) % 100) } }\n\n    private val stateMutator = scope.stateFlowMutator(\n        initialState = Snail7State(),\n        started = SharingStarted.WhileSubscribed(),\n        inputs = listOf(\n            speedChanges,\n            progressChanges,\n        )\n    )\n\n    val state: StateFlow\u003cSnail7State\u003e = stateMutator.state\n\n    fun setSnailColor(index: Int) = stateMutator.launch {\n        emit { copy(color = colors[index]) }\n    }\n\n    fun setProgress(progress: Float) = stateMutator.launch {\n        emit { copy(progress = progress) }\n    }\n}\n```\n\n### `CoroutineScope.actionStateFlowMutator`\n\nThe `actionStateFlowMutator` function transforms a `Flow` of `Action` into a `Flow` of `State` by first\nmapping each `Action` into a `Mutation` of `State`, and then reducing the `Mutations` into an\ninitial state within the provided `CoroutineScope`.\n\nThe above is typically achieved with the `toMutationStream` extension function which allows for\nthe splitting of a source `Action` stream, into individual streams of each `Action` subtype. These\nsubtypes may then be transformed independently, for example, given a sealed class representative of\nsimple arithmetic actions:\n\n```kotlin\nsealed class Action {\n    abstract val value: Int\n\n    data class Add(override val value: Int) : Action()\n    data class Subtract(override val value: Int) : Action()\n}\n```\n\nand a `State` representative of the cumulative result of the application of those `Actions`:\n\n```kotlin\ndata class State(\n    val count: Int = 0\n)\n```\n\nA `StateFlow` `Mutator` of the above can be created by:\n\n```kotlin\n        val mutator = scope.actionStateFlowMutator\u003cAction, State\u003e(\n            initialState = State(),\n            started = SharingStarted.WhileSubscribed(),\n            transform = { actions -\u003e\n                actions.toMutationStream {\n                    when (val action = type()) {\n                        is Action.Add -\u003e action.flow\n                            .map {\n                                mutationOf { copy(count = count + value) }\n                            }\n                        is Action.Subtract -\u003e action.flow\n                            .map {\n                                mutationOf { copy(count = count - value) }\n                            }\n                    }\n                }\n            }\n        )\n```\n\nNon trivially, given an application that fetches data for a query that can be sorted on demand. Its\n`State` and `Action` may be defined by:\n\n```kotlin\ndata class State(\n    val comparator: Comparator\u003cItem\u003e,\n    val items: List\u003cItem\u003e = listOf()\n)\n\nsealed class Action {\n    data class Fetch(val query: Query) : Action()\n    data class Sort(val comparator: Comparator\u003cItem\u003e) : Action()\n}\n```\n\nIn the above, fetching may need to be done consecutively, whereas only the most recently received\nsorting request should be honored. A `StateFlow` `Mutator` for the above therefore may resemble:\n\n```kotlin\nval mutator = scope.actionStateFlowMutator\u003cAction, State\u003e(\n    initialState = State(comparator = defaultComparator),\n    started = SharingStarted.WhileSubscribed(),\n    transform = { actions -\u003e\n        actions.toMutationStream {\n            when (val action = type()) {\n                is Action.Fetch -\u003e action.flow\n                    .map { fetch -\u003e\n                        val fetched = repository.get(fetch.query)\n                        mutationOf {\n                            copy(\n                                items = (items + fetched).sortedWith(comparator),\n                            )\n                        }\n                    }\n                is Action.Sort -\u003e action.flow\n                    .mapLatest { sort -\u003e\n                        mutationOf {\n                            copy(\n                                comparator = sort.comparator,\n                                items = items.sortedWith(comparator)\n                            )\n                        }\n                    }\n            }\n        }\n    }\n)\n```\n\nIn the above, by splitting the `Action` `Flow` into independent `Flows` of it's subtypes,\n`Mutation` instances are easily generated that can be reduced into the current `State`.\n\nA more robust example can be seen in the [Me](https://github.com/tunjid/me) project.\n\n#### Nuanced use cases\n\nSometimes when splitting an `Action` into a `Mutation` stream, the `Action` type may need to be\nsplit by it's super class and not it's actual class. Take the following `Action` and `State`\npairing:\n\n```kotlin\ndata class State(\n    val count: Double = 0.0\n)\n\nsealed class Action\n\nsealed class IntAction: Action() {\n    abstract val value: Int\n\n    data class Add(override val value: Int) : IntAction()\n    data class Subtract(override val value: Int) : IntAction()\n}\n\nsealed class DoubleAction: Action() {\n    abstract val value: Double\n\n    data class Divide(override val value: Double) : DoubleAction()\n    data class Multiply(override val value: Double) : DoubleAction()\n}\n```\n\nBy default, all 4 `Actions` will need to have their resulting `Flows` defined. To help group them\ninto `Flows` of their super types, a `keySelector` can be used:\n\n```kotlin\nval actions = MutableSharedFlow\u003cAction\u003e()\n\nactions\n    .toMutationStream(\n        keySelector = { action -\u003e\n            when (action) {\n                is IntAction -\u003e \"IntAction\"\n                is DoubleAction -\u003e \"DoubleAction\"\n            }\n        }\n    ) {\n        when (val type = type()) {\n            is IntAction -\u003e type.flow\n                .map { it.mutation }\n            is DoubleAction -\u003e type.flow\n                .map { it.mutation }\n        }\n    }\n```\n\nIn the above the two distinct keys map to the `IntAction` and `DoubleAction` super types allowing\nfor granular control of the ensuing `Mutation` stream.\n\nUltimately a `Mutator` serves to produce a stream of `State` from a stream of `Actions`,\nthe implementation of which is completely open ended.\n\n## License\n\n    Copyright 2021 Google LLC\n    \n    Licensed under the Apache License, Version 2.0 (the \"License\");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n    \n        https://www.apache.org/licenses/LICENSE-2.0\n    \n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distributed on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n\n[badge-android]: http://img.shields.io/badge/-android-6EDB8D.svg?style=flat\n\n[badge-jvm]: http://img.shields.io/badge/-jvm-DB413D.svg?style=flat\n\n[badge-js]: http://img.shields.io/badge/-js-F8DB5D.svg?style=flat\n\n[badge-js-ir]: https://img.shields.io/badge/support-[IR]-AAC4E0.svg?style=flat\n\n[badge-nodejs]: https://img.shields.io/badge/-nodejs-68a063.svg?style=flat\n\n[badge-linux]: http://img.shields.io/badge/-linux-2D3F6C.svg?style=flat\n\n[badge-windows]: http://img.shields.io/badge/-windows-4D76CD.svg?style=flat\n\n[badge-wasm]: https://img.shields.io/badge/-wasm-624FE8.svg?style=flat\n\n[badge-apple-silicon]: http://img.shields.io/badge/support-[AppleSilicon]-43BBFF.svg?style=flat\n\n[badge-ios]: http://img.shields.io/badge/-ios-CDCDCD.svg?style=flat\n\n[badge-mac]: http://img.shields.io/badge/-macos-111111.svg?style=flat\n\n[badge-watchos]: http://img.shields.io/badge/-watchos-C0C0C0.svg?style=flat\n\n[badge-tvos]: http://img.shields.io/badge/-tvos-808080.svg?style=flat\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftunjid%2Fmutator","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftunjid%2Fmutator","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftunjid%2Fmutator/lists"}