{"id":22370379,"url":"https://github.com/ianrumac/redukks","last_synced_at":"2025-10-15T06:30:42.815Z","repository":{"id":151284380,"uuid":"622615704","full_name":"ianrumac/redukks","owner":"ianrumac","description":"A set of simple, extendable architecture components to build uniflow \u0026 Redux-like architectures for Kotlin Multiplatform","archived":false,"fork":false,"pushed_at":"2023-04-16T23:04:35.000Z","size":231,"stargazers_count":29,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2024-12-04T19:49:30.347Z","etag":null,"topics":["android","architecture","architecture-components","ios-swift","kotlin","kotlin-library","kotlin-multiplatform","mobile-development","multiplatform","redux","state-management","uniflow","uniflow-kt"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ianrumac.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","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":"2023-04-02T16:24:58.000Z","updated_at":"2023-08-08T09:51:08.000Z","dependencies_parsed_at":null,"dependency_job_id":"0f192cd5-25af-401b-86f6-9cdbc60ef2c1","html_url":"https://github.com/ianrumac/redukks","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ianrumac%2Fredukks","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ianrumac%2Fredukks/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ianrumac%2Fredukks/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ianrumac%2Fredukks/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ianrumac","download_url":"https://codeload.github.com/ianrumac/redukks/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":236582833,"owners_count":19172434,"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","architecture-components","ios-swift","kotlin","kotlin-library","kotlin-multiplatform","mobile-development","multiplatform","redux","state-management","uniflow","uniflow-kt"],"created_at":"2024-12-04T19:45:03.697Z","updated_at":"2025-10-15T06:30:37.518Z","avatar_url":"https://github.com/ianrumac.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Redukks\n\nThis library is in alpha.\nThe API is mostly complete and will not be changing, but there is still work to be done to make it production ready on all platforms.\nFor JVM and multiplatform, the library is ready for production use, but for iOS, JS and native it still isn't completely set up.\nIf you want to help, feel free to open an issue or a PR.\n\n[![Maven Central](https://img.shields.io/maven-central/v/com.ianrumac.redukks/redukks)](https://search.maven.org/artifact/com.ianrumac.redukks/redukks)\n[![Redukks](https://img.shields.io/badge/version-0.1.4-blue)](https://github.com/ianrumac/redukks)\n[![Kotlin](https://img.shields.io/badge/kotlin-1.8.0-blue?logo=kotlin)](https://kotlinlang.org)\n![Weekly](https://androidweekly.net/issues/issue-566/badge)\n\n### A simple, type-safe, and testable redux/uniflow architecture implementation for Kotlin Multiplatform\n\nRedukks is a set of simple type-safe abstractions needed to implement uniflow and Redux-like architecture on Kotlin Multiplatform.\nIt simplifies the creation of stores, reducers and actions, and provides a simple way to test them.\n\n\n### Why Redukks?\n\nWhile there are many uniflow or redux implementations for Kotlin/Kotlin Multiplatform, most of them are either too complex, or too\nsimple. Either the naming is too \"reduxy\", they force you into one way of doing things or the abstractions are\ntoo complex to be written on the regular. And while there are architectures that conform to this style, the naming they use is quite specific for Android and confusing to non-android developers,\nmaking it harder to reason about or discuss with other developers.\n\nSince I've found myself re-using the same set of abstractions through the years, I've decided to make them into a library I can both re-use and share.\n\nThe goal is to provide simple, type-safe, and testable abstractions for redux-like architecture,\nwhile still not tying you completely to a single pattern, providing you with abstractions that can help you build your\nown solution. While most libraries try to push for a full-on redux pattern, reducers can be an overhead\nfor some simple cases, and you should be able to avoid them if you want to - that's why Redukks doesn't *enforce* the Redux pattern itself.\n\nFor more reasoning on why uniflow and why redux, you can check out the talk [(de)constructing architecture](https://speakerdeck.com/ianrumac/de-constructing-architecture).\n\n### What problems can it help me solve?\n\n\n* **State Management**: Redukks provides a simple way to manage your state, update it in a predictable way and listen to changes. You can use\n  it to manage the state of your whole app, a single feature, have a shared state between different features (screens) or have multiple states for one screen.\n  \n\n\n* **Action handling**: Redukks provides a simple way to handle actions, and execute them in a predictable way. You can use it to handle\n  actions from the UI, network calls, or any other work you might need. The unopinionated nature of the library allows you to implement your own\naction handlers specific to your usecase - even letting you break the uniflow pattern by implementing backwards signalling.\n\n\n* **Testing**: With Redukks, testing your state, reducers and actions is incredibly simple - both due to the nature of the API and the extra compile safety. You can easily test your state management logic,\n  or your action handling logic without the need for complex testing API's. \n\n\n## How uniflow/redux works?\n\nThe uniflow architecture is based on one simple idea - your app is a constant cycle of data that flows in one direction.\nThe state and it's changes are driven by the UI, and the UI is driven by the state. This allows you to have a single source of truth\nand provides you with a simple way to think and reason about your app.\n\nMeanwhile, Redux is a pattern that helps you manage your state. It's based on the idea that your state is a single source of truth,\nand that it should be updated in a predictable manner.\n\nRedukks is a combination of these two ideas - it provides you the basic abstractions to implement\nuniflow and Redux-based architectures but doesn't force you into a single way of doing things.\n\n![\"How redux works\"](./redux.png)\n\n\nThe flow itself is quite simple - you have a state X and you have N ways to change it.\nThose ways to change it are called \"reducers\" and they are the only way to change that state.\nSo instead of wildly updating it from wherever we please, we define a set of \"Updates\" that can be used to update the state.\n\nBut, since most applications are more complex than a simple counter, you need a way to handle complex work too.\nThat's where actions come in. You \"dispatch\" an action and it executes some work, and during that execution, it can update the state via reducers or invoke other actions.\n\nThis makes it easy to define and test all the possible permutations of state changes, and makes it easy to reason about the state of your app and be confident it works how you intended.\n\n\n\n## Quickstart\n\n1. Add the dependency to your project\n\nGradle Groovy:\n\n```groovy\ndependencies {\n    implementation 'com.ianrumac.redukks:redukks:0.1.4'\n}\n```\n\nGradle Kotlin:\n\n```kotlin\n    implementation(\"com.ianrumac.redukks:redukks:0.1.4\")\n```\n\n2. Define a state type and it's updates\n\n```kotlin\ndata class CountState(val total: Int)\n\nsealed class Updates(override val reduce: (CountState) -\u003e CountState) : Reducer\u003cCountState\u003e {\n    class Add(val number: Int) : Updates({\n        state.copy(state.total + number)\n    })\n    class Subtract(val number: Int) : Updates({\n        state.copy(state.total - number)\n    })\n}\n```\n\n3. Create a `FeatureContext` class that contains all the dependencies for your feature (or for your actions).\n\n```kotlin\ninterface CountContext {\n    val client: CountingAPI\n    val store: Store\u003cCountState\u003e\n}\n```\n\n4. Define `Actions` that can be executed on the context\n\n```kotlin\nsealed class Actions(override val execute: suspend CountContext.() -\u003e Unit) : TypedAction\u003cCountContext\u003e {\n    class AddToClient(val number: Int) : Actions({\n        //here, we have access to all the CountContext properties that are captured during execution\n        //For example, we can access the store and update it with the result of the network call.\n        val result = client.add(number)\n        state.update(Updates.Add(result))\n    })\n\n    class SubtractFromClient(val number: Int) : Actions({\n        val result = client.subtract(number)\n        state.update(Updates.Subtract(result))\n    })\n}\n```\n\n5. Execute actions on the context\n\n```kotlin\n\ndata class CountWithAnAPI(val scope: CoroutineScope) : CountContext {\n    override val client = CountingAPI()\n    override val store = reducedStore(CountState(0))\n    override val handler =\n        actionHandlerFor(scope, this) //this is a coroutine scope that will be used to execute the actions\n}\n\nval context = CountWithAnAPI(scope)\n\n//In a class that implements Dispatcher\u003cActions\u003e\n//This is actually a call to context.handler.dispatch(Actions.Add(1))\ndispatch(Actions.Add(1))\n```\n\n6. To make it easier to read, you can define it all under a context interface:\n\n```kotlin\ninterface CountContext {\n  val client: CountingAPI\n  val store: Store\u003cCountState\u003e\n  val handler: ActionDispatcher\u003cActions\u003e\n\n\n  data class CountState(val total: Int)\n  sealed class Updates(override val reduce: (CountState) -\u003e CountState) : Reducer\u003cCountState\u003e {\n      class Add(val number: Int) : Updates({\n          state.copy(state.total + number)\n      })\n      class Subtract(val number: Int) : Updates({\n          state.copy(state.total - number)\n      })\n  }\n\n\n  sealed class Actions(override val execute: suspend CountContext.() -\u003e Unit) : TypedAction\u003cCountContext\u003e {\n      class AddToClient(val number: Int) : Actions({\n          //here, we have access to all the CountContext properties that are captured during execution\n          //For example, we can access the store and update it with the result of the network call.\n          val result = client.add(number)\n          state.update(Updates.Add(result))\n      })\n\n      class SubtractFromClient(val number: Int) : Actions({\n          val result = client.subtract(number)\n          state.update(Updates.Subtract(result))\n      })\n  }\n}\n```\n\n7. Now, you can use the context to execute actions, and listen to state changes:\n\n```kotlin\nval context = CountWithAnAPI(scope)\nval handler = context.handler\nhandler.dispatch(Actions.AddToClient(1))\ncontext.store.listen().collectLatest { state -\u003e\n    //do something with the state\n}\n```\n\n## For Android developers\n\nYou can easily use Redukks with Android's ViewModel and it fits great with Jetpack Compose (and the View framework too!).\nSimply collect the store as a StateFlow or use it with LiveData. Your ViewModel can also implement `Dispatcher\u003cActions\u003e` so you can dispatch the actions upstream (delegate them to the action handler).\nYou can also create abstractions for this such as a `ReduxViewModel` that handle these things for you.\n\nUsing it with a viewmodel is simple:\n\n```kotlin\n@HiltViewModel\nclass CountViewModel @Inject constructor(\n    val actionHandler: Dispatcher\u003cCountContext.Actions\u003e,\n    val store: Store\u003cCountContext.State\u003e\n) : ViewModel(), Dispatcher\u003cActions\u003e by actionHandler {\n    //Note: Not a real implementation, be careful where you collect and map your state\n    val state = store.listen().map(this::mapToViewModel)\n        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), initialValue = CountVM(0))\n\n    fun mapToViewModel(it: CountContext.State): CountVM {\n        ...\n    }\n}\n```\n\nAnd now in your UI you can simply do:\n\n```kotlin \n val vm : CountViewModel by viewModels()\n  ...\n vm.dispatch(Actions.AddToClient(1))\n```\n\nYou can also control the lifecycle of your state by hoisting it up or down in the lifecycle scope tree.\nFor example, if you need to share a state store, you can just move it up from i.e. a fragment scope to an activity scope\nand pass it downwards to the fragments. This way, you can share the state between fragments, but still have\nonly a single state store. You can even provide multiple stores for an action, or provide multiple stores to your UI to combine.\n\n```kotlin\ntypealias CountStore = Store\u003cCountState\u003e\ntypealias UserStore = Store\u003cUserState\u003e\n\ninterface CountContext {\n    val countStore: CountStore\n    val userStore: UserStore\n}\n\nclass AuthenticatedCounterContext(userStore: UserStore) {\n    override val userStore = userStore\n    override val countStore = CountStore(CountState(0))\n}\n\n//...\nclass AddForUser : Actions({\n    val count = countStore.state.total\n    countStore.update(Updates.Add(1))\n    userStore.update(UpdateStateForUser(countStore.total))\n})\n\n```\n\nThis works beautifully with Jetpack Compose, since you can easily derive the UI from the state (please, map it\nto a viewmodel class before doing so, don't mix your core models and your UI models).\n\n![\"How redux works on Android\"](./redux_android.png)\n\n## General Usage\n\nSince Redukks is a modular set of abstractions, you can use only the parts you need for the complexity level you want.\nRedukks provides a few default implementations of the abstractions, but you can easily create your own.\n\nLet's start with the simplest case, a simple store.\nWe'll start with a simple counter implementation, with only a store without reducers or actions.\nIn general, if you prefer not to write reducers, you can use the `BasicStore` class, which is a simple store exposing\nan `update` function with a closure that takes in the current state and returns a new state.\n```kotlin\n\ndata class CountState(val total: Int)\n\nval store = BasicStore(CountState(0))\n\nstore.update {\n    it.copy(it.total + 1)\n}\n\nprintln(store.state.total) //prints 1\n\nstore.listen().collectLatest { state -\u003e\n    //do something with the state\n}\n```\n\nNow, we can also use a reducer to define state updates in a typesafe way.\nSo let's create a reducer. Reducers are just objects that implement the `Reducer\u003cStateType\u003e` interface,\nmeaning they have a `reduce` closure that takes in the current state and returns a new state.\nIt's like writing\n\n```kotlin\nfun addNumber(state: CountState, number: Int) : CountState {\n    return state.copy(state.total + number)\n}\n```\n\nbut with a bit more type safety, so that you can be certain all possible state changes are predefined and tested.\n\n```kotlin\nsealed class Updates(override val reduce: (CountState) -\u003e CountState) : Reducer\u003cCountState\u003e {\n    class Add(val number: Int) : Updates({\n        state.copy(state.total + number)\n    })\n    class Subtract(val number: Int) : Updates({\n        state.copy(state.total - number)\n    })\n}\n```\n\nNow, to use this reducer, we need to create a `ReducedStore\u003cStateType, ReducerType\u003e`. This is a store that can only be\nupdated\nvia the defined reducer type. A default implementation is provided as BasicReducedStore, but you can easily create your\nown.\n\n```kotlin\nval basicStore = BasicStore(CountState(0))\n// you can either provide it with a store or the initial state to use a BasicStore\nval store = BasicReducedStore\u003cCountState, Updates\u003e(basicStore)\n\n//or use the helper function\nval store = createReducedStore\u003cCountState,Updates\u003e(CountState(0))\n\nstore.update(Updates.Add(1))\nstore.listen().collectLatest { state -\u003e\n    //do something with the state\n}\n```\n\n## Actions\n\nActions are asynchronous pieces of code that can be executed.\nUsually they are used to perform side effects, like network calls, or to dispatch other actions and updates.\nFor example, fetching a list of items from a server, and then dispatching an action to update the state with the result.\n\nActions are a bit more complicated, since they usually need to be able to access dependencies.\nTo do this, we provide a `\u003cContext\u003e` that contains all the dependencies, and then create actions that\nare executed with context as a receiver, meaning they have access to it as `this`.\nWith this, we can dispatch them to a `Dispatcher\u003cActionType\u003e`, which should handle actions.\n\nA basic coroutine based action handler is implemented by default under `TypedActionHandler` and\na custom one can be easily implemented by providing a `Dispatcher\u003cActionType\u003e` or `AsyncDispatcher\u003cActionType\u003e` for more\nexecution control (via Deferred\u003cUnit\u003e). Also, provided is a `AsyncAction` class that can be used to simply pass\nthe closure via constructor.\n\nNow, let's see how to define async actions.\n\nFirst, we need a context that these actions can be executed on.\n\nWhile for simple cases (i.e. simply updating the store) you can just use a `Store\u003cStateType\u003e`, it's better to create a\ncustom context.\nThis gives you an easy way to access dependencies, mock them, replace them, etc. Also, it's a good practice to keep\nthe actions tightly coupled to them, so that you can easily see what actions need what dependencies.\n\nWith Kotlin's context receivers on the way, the code can be even cleaner in the future,\nremoving the need for actions entirely and just implementing simple functions upon the receivers. But since most\ncodebases don't have context receivers yet, we'll focus only on this implementation for now.\n\nFirst, we define a CountContext interface that contains the dependencies we need:\n\n```kotlin\ninterface CountContext {\n    val client: CountingAPI\n    val store: Store\u003cCountState\u003e\n    val handler: Dispatcher\u003cActions\u003e\n}\n```\n\nThen, we can define Actions via sealed classes that implement `TypedAction\u003cCountContext\u003e`:\n\n```kotlin\nsealed class Actions(override val execute: suspend CountContext.() -\u003e Unit) : TypedAction\u003cCountContext\u003e {\n    class Add(val number: Int) : Actions({\n        //here, we have access to all the CountContext properties that are captured during execution\n        //For example, we can access the store and update it with the result of the network call.\n        val result = client.add(number)\n        state.update(Updates.UpdateState(result))\n    })\n\n    class Subtract(val number: Int) : Actions({\n        val result = client.subtract(number)\n        state.update(Updates.UpdateState(result))\n    })\n}\n```\n\nNow, we can implement the context and use the dispatcher to execute actions:\n\n```kotlin\ndata class CountWithAnAPI(val scope: CoroutineScope) : CountContext {\n    override val client = CountingAPI()\n    override val store = BasicReducedStore\u003cCountState, Updates\u003e(CountState(0))\n    override val handler = createActionHandler(scope, this)\n}\n\n//In a class that implements Dispatcher\u003cActions\u003e, like a viewmodel that delegates it to the handler\n//This is actually a call to context.handler.dispatch(Actions.Add(1))\n\ndispatch(Actions.Add(1))\ndispatch(Actions.Subtract(1))\n\n```\n\n## Testing\n\nTesting is quite easy, for both reducers and actions. For reducers, you can simply create a state and a reducer,\ni.e. `val state = CountState(0)` and `val reducer = Updates.Add(1)`, and then call `reducer.reduce(state)`.\nNow you can assert that the result is the expected one.\n\n```kotlin\n@Test\nfun `update the total count by adding 1`() {\n    val state = CountState(0)\n    val reducer = Updates.Add(1)\n    val result = reducer.reduce(state)\n    assertEquals(1, result.total)\n}\n```\n\nFor actions, you can create a context and a dispatcher, i.e. `val context = CountWithAnAPI(TestCoroutineScope())`\nand `val action = Action.Add(1)`, and then call `action.execute(context)`.\nThis provides you with an easy way to test actions, since you can easily mock the context and assert that the\nactions are executed as expected or use mocking libraries such as MockK to verify mocks executed properly.\n\n```kotlin\n@Test\nfun `update the total count and add from client`() = runTest {\n        val context = CountWithAnAPI(this)\n        val action = Actions.Add(1)\n        action.execute(context)\n        assertEquals(1, context.store.state.total)\n    }\n```\n\n## Customization\n\nEach component is written in a way that allows for easy customization, so that you can easily create your own.\nMeaning by implementing a Store or a Dispatcher, you can easily have things like an API/Socket/Room backed store,\nor Actions that signal when they are done, or other Flows in your store.\nDon't be afraid to customize the pattern to your needs - we're not writing code to satisfy the patterns,\nwe're writing it for ourselves, our teammates and our users.\n\n## Contributing\n\nFeel free to open an issue or a PR if you have any suggestions or improvements.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fianrumac%2Fredukks","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fianrumac%2Fredukks","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fianrumac%2Fredukks/lists"}