{"id":19502763,"url":"https://github.com/ml-opensource/kedux","last_synced_at":"2025-04-26T00:32:40.265Z","repository":{"id":106622541,"uuid":"250022335","full_name":"ml-opensource/kedux","owner":"ml-opensource","description":"A kotlin multiplatform Redux implementation utilizing Kotlin Coroutines and Flow :heart:","archived":false,"fork":false,"pushed_at":"2020-09-10T13:38:46.000Z","size":7893,"stargazers_count":7,"open_issues_count":3,"forks_count":0,"subscribers_count":5,"default_branch":"master","last_synced_at":"2024-11-10T22:18:09.636Z","etag":null,"topics":["android","ios","js","kotlin-multiplatform","linux","macos","redux","swift","t","typescript","web","windows"],"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/ml-opensource.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":"2020-03-25T15:51:08.000Z","updated_at":"2024-05-13T15:52:20.000Z","dependencies_parsed_at":null,"dependency_job_id":"2548ea3c-79b5-488b-ab62-3527213b628d","html_url":"https://github.com/ml-opensource/kedux","commit_stats":null,"previous_names":["ml-opensource/kedux"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ml-opensource%2Fkedux","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ml-opensource%2Fkedux/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ml-opensource%2Fkedux/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ml-opensource%2Fkedux/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ml-opensource","download_url":"https://codeload.github.com/ml-opensource/kedux/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":224019778,"owners_count":17242255,"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","ios","js","kotlin-multiplatform","linux","macos","redux","swift","t","typescript","web","windows"],"created_at":"2024-11-10T22:18:09.994Z","updated_at":"2024-11-10T22:18:18.810Z","avatar_url":"https://github.com/ml-opensource.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Kedux\n\n![badge][badge-android]\n![badge][badge-ios]\n![badge][badge-js]\n![badge][badge-jvm]\n![badge][badge-mac]\n\nKedux is a Kotlin-multiplatform implementation of [redux](https://redux.js.org) that works \non Android, iOS, MacOS, and JS utilizing Coroutines + Flow :heart:\n \n __NOTE: This library is currently in development preview. Please check back later when it's ready for release.__\n\n## Getting Started\n\nThis library provides it's \"best\" API for each translated platform as much as possible. The following guides provide getting started for \nusers of other platforms including common Kotlin code as a dependency.\n\n[Swift Guide](/docs/Swift.md)\n\n[Typescript Guide](/docs/Typescript.md) (TBD)\n\n\n### State\n\nState in Kedux should be immutable. This means utilizing `data class`, `List`, `Map`, and \nall immutable constructs Kotlin provides.\n\nThe `State` object should represent your application's state. *Note*: Kedux supports `FracturedState` for apps that want to split up \nstate into multiple modules.\n\nSay we have a simple state as:\n\n```kotlin\ndata class Product(val id: Int, val name: String)\ndata class Location(val id: Int, val other: String, val product: Product? = null)\ndata class GlobalState(val name: String, val location: Location? = null)\n\n// define an initial state (required)\n// gets emitted on first selector subscription.\nval initialState = GlobalState(name = \"\", location = null)\n``` \n\n### Reducer\n\nNext define a set of actions. These actions should be `sealed class` type so we can do some magic.\n\n```kotlin\nsealed class StoreTestAction {\n\n    data class NameChange(val name: String) :\n        StoreTestAction()\n\n    data class LocationChange(val location: Location) :\n        StoreTestAction()\n\n    // objects are nice\n    object Reset : StoreTestAction()\n}\n``` \n\nNext, we define our global reducer for our state. Kedux supports reducers from global state, fractured state, \nand on specific action types automatically.\n\n```kotlin\nval sampleReducer = typedReducer\u003cGlobalState, StoreTestAction\u003e { state, action -\u003e\n    // due to action sealed class, compiler can verify all action type args!\n    // under the hood, this inline function typedReducer verifies the action is the proper type \n    // before arriving here.\n    when (action) {\n        is StoreTestAction.NameChange -\u003e state.copy(name = action.name)\n        is StoreTestAction.Reset -\u003e state\n        is StoreTestAction.LocationChange -\u003e state.copy(location = action.location)\n    }\n}\n```\n\nAs you would for Redux, construct your store somewhere accessible.\n\nWe recommend using dependency injection to provide your store in your app so it's easier to test.\n\n```kotlin\n\nval store =  createStore(sampleReducer, initialState)\n\n```\n\n#### Logging\n\n`Store.loggingEnabled`: set this to log events to the native console as they come in. Preferrably this is set only on development \nbuilds.\n\n### Selectors\n\nSelectors are pure `Observable` functions that accept state and emit changes to the state.\n\nSelectors _only_ emit when their output is distinct. \n\nSelectors _only_ recompute when their upstream state is distinct.\n\nSelectors emit on subscription. So dowstream observers will receive the latest state.\n\nSelectors can be combined.\n\nTo observe changes on the store, subscribe to its state:\n\n```kotlin\nval nameSelector = createSelector\u003cGlobalState, String\u003e { state -\u003e state.name }\n\n// subscribe to store updates\nstore.select(nameSelector).onEach { name -\u003e\n  // do something with name\n}.launchIn(scope) \n```\n\nIt's important to ensure you add the subscription to a `CoroutineScope`, \nso that you do not introduce memory leaks.\n\n## Features\n\nThis library has a few features. TBD on full descriptions.\n\n## Store\n\n`Store` is an object that exposes a `state: Flow\u003cState\u003e` in which subscribers can listen to state changes, and an actions stream\n `action: Flow\u003cAction\u003e` that logs all actions coming through. \n\n`createStore`: creates the store with a global `reducer`, initialState (required), and `enhancer` (more on these later).\n \n`Store.dispatch`: **asynchronously** dispatches actions to the `state`. Selection happens on the `computationScheduler`, \nand then returns the result object on the `mainScheduler` thread of the platform.\n\n`Store.loggingEnabled`: a global value to turn on or off logging on the store for all actions and effects.\n\n__Note__: Kotlin Native targets should be wary of frozen objects when passing between threads. By design, state should be \nimmutable in this library's constructs to prevent `InvalidMutabilityException` errors.\n\n### Supported Action Types\n\nOutside of plain objects, you can also `dispatch` special objects on the `Store` if you wish.\n\nSupported types:\n\n`1`. `Pair` - dispatches both actions on the store in order.\n```kotlin\nstore.dispatch(MyAction() to MyAction2())\n```  \n\n`2`. `Triple` - dispatches all three actions on the store in order.\n```kotlin\nstore.dispatch(Triple(MyAction(), MyAction2(), MyAction3()))\n```\n\n`3`. `MultiAction` - dispatches 0 to N actions on the store in order.\n\n```kotlin\nstore.dispatch(multipleActionOf(MyAction(), MyAction2(), MyAction3(), MyActionN()))\n```\n\n`4`. `NoAction` - store will not dispatch the action. Useful for `Effects` that are silent, or within a `when` returns that \nreturn an Action type based on conditions and you want to ignore the action:\n\n```kotlin\nstore.dispatch(when(name) {\n    \"first\" -\u003e FirstNameChanged(name) \n    \"middle\" -\u003e MiddleNameChanged(name)\n    \"last\" -\u003e LastNameChanged(name)\n    else -\u003e NoAction\n})\n```\n\n`5`. `Action\u003cT\u003e` - actions based on a type argument to distinguish them. Rather instead of using plain Action `data class` objects, \nyou can create actions as functions:\n\n```kotlin\n\n// no arguments or payload immediately create action (to get around passing `Unit` to `ActionCreator`)\nval loadUsersAction = createAction(\"[Users] Load Users\")\n\n// use on store\nstore.dispatch(loadUsersAction)\n\n// ActionCreator with `Int` argument, that returns an action with `Int` payload incremented by 1.\nval loadUserAction = createAction(\"[Users] Load User by Id\") { argument: Int -\u003e argument + 1 }\n\n// use on store\nstore.dispatch(loadUserAction(5))\n\n// ActionCreator that accepts no arguments but allows payload return:\nval loadUserActionDefault = createAction(\"[Users] Load User by Id Default\") { 1 }\n\n// use on Store\nstore.dispatch(loadUserActionDefault())\n\n```\n\n## Reducers\n\nThere are three main kinds of reducers.\n\n`anyReducer`: constructs a reducer on the whole global store, without specifying action type. This is useful when your reducer \nconsumes multiple action classes. You will need to handle default case in this instance.\n\n`typedReducer` (preferred): constructs a reducer that will only run when the `Action` class type is of the type specified. So \nthat a safer consumption occurs. I.e. the reducer only executes when the action type is a subtype of the expected action type.\n\n```kotlin\nval sampleReducer = typedReducer\u003cGlobalState, StoreTestAction\u003e { state, action -\u003e\n    when (action) {\n        is StoreTestAction.NameChange   -\u003e state.copy(name = action.name)\n        is StoreTestAction.Reset -\u003e state\n        is StoreTestAction.LocationChange -\u003e state.copy(location = action.location)\n        is StoreTestAction.NamedChanged -\u003e state.copy(nameChanged = true)\n        is StoreTestAction.LocationChanged -\u003e state\n        // using data classes, compiler doesn't need an `else` branch.\n    }\n}\n```\n\n`actionTypeReducer`: constructs a reducer that will only run when the `Action.type` matches the type specified in the reducer. \nThis is useful for `createAction` results by function and switching on the type you want to consume.\n\n```kotlin\nval sampleTypedReducer = actionTypeReducer { state: GlobalState, action: Action\u003cSampleEnumType, out Any\u003e -\u003e\n    when (action.type) {\n        SampleEnumType.LocationChange -\u003e state.copy(location = action.payload as Location?)\n        SampleEnumType.NameChange -\u003e state.copy(name = action.payload as String)\n        SampleEnumType.Reset -\u003e initialState\n        // use enum for action type tokens ensures compiler doesnt need an `else` branch.\n    }\n}\n```\n\n`combineReducers`: Combines multiple reducers to listen on the same state object. \n\n## Selectors\n\nSelectors are functions that are memoized with their input data and only recompute when the state changes. \nThey are useful for heavy calculations such as retrieving an object out of a list by id, for example.\n\nCreating a selector is easy:\n```kotlin\n// declare a global field, selectors are just functions\nval fieldSelector = createSelector\u003cGlobalState, Field\u003e { state -\u003e state.field }\n\n// subscribe to the selector to gain new values\nstore.select(fieldSelector).onEach { value  -\u003e\n  // do something\n}\n.launchIn(scope)\n```\n\nSelectors can be composed. Each nested level only recomputes when its outer state changes. It's best practice \nto break up the composition into smaller pieces.\n\n```kotlin\n// avoid\nval nameSelector = createSelector\u003cGlobalState, Location?\u003e { state -\u003e state.location }\n    .compose { state -\u003e state.product}\n    .compose { state -\u003e state.name }\n\n// preferred defining them top-level and chaining them, just in case you need more :)\nval locationSelector = createSelector\u003cGlobalState, Location?\u003e  { state -\u003e state.location }\nval productSelector = locationSelector.compose { state -\u003e state.product }\nval productNameSelector = productSelector.compose { state -\u003e state.name }\n```\n\nBy composing selectors in separate fields, they become more reusable.\n\n## Effects / Sagas\n\nEffects are `Flow` chains that occur after an action is dispatched on the store, and return with \nanother action, set of actions (`MultiAction`), or `NoAction`.\n\nTo define an `Effect`:\n\n```kotlin\nval getUsersEffect = createEffect\u003cLoadUsers, UsersReceived\u003e { actionObservable -\u003e\n    actionObservable.flatMap { (userId) -\u003e userService.getUsers(userId) }\n        .map { users -\u003e UsersReceived(users) }\n}\n```\n\nIn this example, the `Effect` responds to a `LoadUsers` action, calls out to `UserService`, and returns a `UsersReceived` \naction, which the store dispatches out to a `Reducer` to handle. \n\n__Pro Tip__: Be careful of cyclical `Effect`. If you have two separate effects consume and dispatch each other's effects, \nyou could run into a cycle that consumes your application and might cause it to freeze.\n\nNow group the `Effect` into an `Effects` object:\n\n```kotlin\nval usersEffects = Effects(getUsersEffect)\n```\n\nAn `Effects` object manage the scoped lifecycle and binding to the `Store` actions. They efficiently \ngroup the bindings together into logical components.\n\n`Effects` are bound to the `Store` in a couple of ways: globally and scoped.\n\nGlobally - bind to the `Store` in global scope when the `Store` is created:\n\n```kotlin\nstore = createStore(...)\n    .also { usersEffects.bindTo(it) }\n ```\n\nOr Scope Effect groupings at a smaller level, such as within a particular flow in your application:\n```kotlin\n\nval usersEffects = Effects(getUsersEffect, effect2, effectN)\n\n// bind to store when object in scope\nuserEffect.bindTo(store)\n\n// remove subscriptions to Store when out of scope.\nuserEffect.clearBindings()\n\n```\n\n### Multiple Actions\n\n`Effects` can return multiple effects at a time in a fan-out fashion. This is very useful \nwhen you want keep your actions pure, such as notifying a `Reducer` of a loading state change, while another `Reducer`  \nreceives the actual data.\n\n```kotlin\nval multipleDispatchEffect = createEffect\u003cLocationChange, MultiAction\u003e { change -\u003e\n    change.map { (location) -\u003e multipleActionOf(LocationChanged(location.other), LoadStatus.Done) }\n}\n```\n\nAll types specified in `Store` are supported as return types in `Effects`.\n\n## Advanced Features\n\n## Enhancers\n\nEnhancers enable you to transform an action as they come in and go to `dispatch`. They enable \nyou to dispatch multiple actions outside the normal single-dispatch action. \n\n```kotlin\ncreateStore(reducer, initialState, enhancer = DevToolsEnhancer()) // just an example\n```\n\n__combining enhancers__: coming soon.\n\n### Fractured States\n\nFractured state is when we want to have our reducers only respond to state changes on a single field from the `GlobalState` \nvariable. This is accomplished using the `FracturedState` object and special creation of our store:\n```kotlin\n store = createFracturedStore(\n            productReducer reduce Product(0, \"\"),\n            locationReducer reduce Location(0, \"\")\n        )\n```\nThis method returns a `Store\u003cFracturedState\u003e` with a few helper extensions to make usage cleaner.\n`FracturedState` is essentially a reducer-class to object map. \n\n`reduce` is an `infix` convenience to enforce unified object type between our reducer and default state of its fractured state. This \nis impossible to enforce using the `Pair` class directly, so use `reduce` instead of `to`.\n\n`productReducer` looks like:\n```kotlin\n// has a different set of actions, and use top-level object we want to grab\nval productReducer = typedReducer\u003cProduct, ProductActions\u003e { state, action -\u003e\n    when (action) {\n        is ProductActions.NameChange -\u003e state.copy(name = action.name)\n    }\n}\n```\n\nNow we can subscribe to the changes via:\n```kotlin\n store.select(fracturedSelector(productReducer))\n  .onEach { value -\u003e\n   // do something with Product                 \n }.launchIn(scope)\n```\n\nThe `fracturedReducer` will loop through each reducer to determine any state changes and update subscribers across the fractured state map. \n\nNesting `fracturedReducer` is not supported, though `compose`-ing is supported.\n\n### Loading States\n\nTypically to represent loading state you might create an object to represent success, error, loading, and result.\n\nKedux provides a convenience object `KeduxLoader` to represent all actions, a reducer to handle state changes, and an effect to coordinate \nthe loading, success, and error states.\n\nAlso, `KeduxLoader` supports clearing state via `loader.clear` action type.\n\n```kotlin\nval userLoadingState = KeduxLoader\u003cInt, User\u003e(\"user\") { id -\u003e userService.getUser(id) }\n\n// request action\nstore.dispatch(userLoadingState.request(5))\n\n// resets state back to LoadingModel.empty()\nstore.dispatch(userLoadingState.clear)\n\n// can manually call if you dont want the default effect\nstore.dispatch(userLoadingState.success(user))\nstore.dispatch(userLoadingState.error(error))\n\n// you must use the LoadingModel object to represent it's state.\ndata class State(val user: LoadingModel\u003cUser\u003e = LoadingModel.empty())\n\n// define selectod\nval userLoadingStateSelector = createSelector { state: State -\u003e state.user }\n// only emits if success is not null\nval userSuccess = userLoadingStateSelector.success()\nval userOptionalSuccess = userLoadingStateSelector.optionalSucces()\n// only emits if error is not null\nval userError = userLoadingStateSelector.error()\nval userOptionalError = userLoadingStateSelector.optionalError()\n\n// convenience extensions on selectors\nstore.select(userSuccess)\n .onEach { success -\u003e\n  // only returns if there's a success value\n }\n .launchIn(scope)\n```\n\nSince we want to avoid reflection, using the `KeduxLoader` reducer requires a little more magic:\n```kotlin\nval reducer = anyReducer { state: State, action: Any -\u003e\n  when(action) {\n    // catch all Loading action types here and modify state.\n    is LoadingAction\u003c*, *\u003e -\u003e {\n      state.copy(\n         product = loader.reducer.reduce(state.product, action),\n         otherLoading = otherLoader.reducer.reduce(state.otherLoading, action),\n      )\n    }\n  }\n}\n```\n\nWe need to call the reducer manually in this case.\n\n[badge-android]: http://img.shields.io/badge/platform-android-brightgreen.svg?style=flat\n[badge-ios]: http://img.shields.io/badge/platform-ios-lightgrey.svg?style=flat\n[badge-js]: http://img.shields.io/badge/platform-js-yellow.svg?style=flat\n[badge-jvm]: http://img.shields.io/badge/platform-jvm-orange.svg?style=flat\n[badge-linux]: http://img.shields.io/badge/platform-linux-important.svg?style=flat \n[badge-windows]: http://img.shields.io/badge/platform-windows-informational.svg?style=flat\n[badge-mac]: http://img.shields.io/badge/platform-macos-lightgrey.svg?style=flat\n[badge-wasm]: https://img.shields.io/badge/platform-wasm-darkblue.svg?style=flat\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fml-opensource%2Fkedux","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fml-opensource%2Fkedux","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fml-opensource%2Fkedux/lists"}