{"id":20234653,"url":"https://github.com/toggl/komposable-architecture","last_synced_at":"2025-05-16T15:05:13.078Z","repository":{"id":38081888,"uuid":"316494344","full_name":"toggl/komposable-architecture","owner":"toggl","description":"🏗️ Kotlin implementation of Point-Free's composable architecture","archived":false,"fork":false,"pushed_at":"2024-12-03T11:45:03.000Z","size":471,"stargazers_count":291,"open_issues_count":8,"forks_count":23,"subscribers_count":12,"default_branch":"main","last_synced_at":"2025-04-03T13:15:50.616Z","etag":null,"topics":["android","architecture","coroutines-android","kotlin","kotlin-android","kotlin-coroutines","mvi-android"],"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/toggl.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-11-27T12:22:40.000Z","updated_at":"2025-03-26T13:09:30.000Z","dependencies_parsed_at":"2023-12-01T14:00:46.652Z","dependency_job_id":"23ad1535-e5f1-4448-8a1e-a0c4a45a7c1b","html_url":"https://github.com/toggl/komposable-architecture","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/toggl%2Fkomposable-architecture","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/toggl%2Fkomposable-architecture/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/toggl%2Fkomposable-architecture/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/toggl%2Fkomposable-architecture/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/toggl","download_url":"https://codeload.github.com/toggl/komposable-architecture/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248565003,"owners_count":21125413,"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","coroutines-android","kotlin","kotlin-android","kotlin-coroutines","mvi-android"],"created_at":"2024-11-14T08:12:08.905Z","updated_at":"2025-04-12T11:51:34.262Z","avatar_url":"https://github.com/toggl.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 🧩 Komposable Architecture  [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.toggl/komposable-architecture/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.toggl/komposable-architecture) [![Build Status](https://app.bitrise.io/app/8fc708d11fa0a5e5/status.svg?token=Q5m1YqGgX4VrIz4V2d0Olg\u0026branch=main)](https://app.bitrise.io/app/8fc708d11fa0a5e5) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)\nKotlin implementation of [Point-Free's The Composable Architecture](https://github.com/pointfreeco/swift-composable-architecture)\n\n## 🚧 Project Status\nWe've been using the Komposable Architecture in production for years now, and we haven't encountered any major issues. \nHowever, the API is still subject to change, at least until we reach version 1.0. We are working to make the setup more straightforward and are considering ways to integrate Jetpack Navigation as well.\n\n## 💡 Motivations\nWhen it came time to rewrite Toggl's mobile apps, we chose a native approach instead of continuing with Xamarin.\nWe quickly realized that, despite the apps not sharing a common codebase, we could still share many aspects across them.\nUsing the same architecture allowed us to share specs, GitHub issues, and create a single common language that both Android and iOS developers can use.\nThis approach has even sped up the development of features already implemented on the other platform!\n\nWe chose to use [Point-Free](https://www.pointfree.co/)'s Composable Architecture as the apps's architecture, which meant we had to set out to implement it in Kotlin. This repo is the result of our efforts!\n\n## 🍎 Differences from iOS\n\nWhile all the core concepts are the same, the composable architecture is still written with Swift in mind, which means not everything can be translated 1:1 to Kotlin. Here are the problems we faced and the solutions we found:\n\n### No KeyPaths\nThe lack of KeyPaths in Kotlin forces us to use functions in order to map from global state to local state.\n\n### No Value Types\nThere's no way to simply mutate the state in Kotlin like the Composable architecture does in Swift. Instead, the reduced state is returned from the reducer along with any effects in [`ReduceResult`](https://github.com/toggl/komposable-architecture/blob/main/komposable-architecture/src/main/java/com/toggl/komposable/architecture/ReduceResult.kt).\n\n### Subscriptions\nAdditionally we decided to extend Point-Free architecture with something called subscriptions. This concept is taken from the [Elm Architecture](https://guide.elm-lang.org/architecture/). It's basically a way for us to leverage observable capabilities of different APIs, in our case it's mostly for observing data stored in [Room Database](https://developer.android.com/training/data-storage/room).\n\n## 📲 Sample App\n- [Todo Sample](https://github.com/toggl/komposable-architecture/tree/main/samples/todos)\n- More samples soon!\n\nTo run the sample app, start by cloning this repo:\n\n```\ngit clone git@github.com:toggl/komposable-architecture.git\n```\n\nNext, open Android Studio and open the newly created project folder. You'll want to run the todo-sample app.\n\nFor more examples take a look at [Point-Free's swift samples](https://github.com/pointfreeco/swift-composable-architecture#examples)\n\n## 🚀 Installation\nThe latest release is available on [Maven Central](https://search.maven.org/artifact/com.toggl/komposable-architecture/1.0.0-preview04/jar).\n\n```kotlin\nimplementation(\"com.toggl:komposable-architecture:1.0.0-preview04\")\ntestImplementation(\"com.toggl:komposable-architecture-test:1.0.0-preview04\") // optional testing extensions\nksp(\"com.toggl:komposable-architecture-compiler:1.0.0-preview04'\")  // optional compiler plugin (still experimental)\n```\n\n## © Licence\n\n```\nCopyright 2021 Toggl LLC\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\n## 🧭 High-level View\n\n\u003e [!WARNING]  \n\u003e This documentation applies to version 1.0, currently in preview.\n\nThis is a high level overview of the different parts of the architecture. \n\n- **Views** This is anything that can subscribe to the store to be notified of state changes. Normally this happens only in UI elements, but other elements of the app could also react to state changes.\n- **Action** Simple structs that describe an event, normally originated by the user, but also from other sources or in response to other actions (from Effects). The only way to change the state is through actions. Views send actions to the store which handles them in the main thread as they come.\n- **Store** The central hub of the application. Contains the whole state of the app, handles the actions, passing them to the reducers and fires Effects.\n- **State** The single source of truth for the whole app. This data class will be probably empty when the application start and will be filled after every action. \n- **Reducers** Reducers are pure functions that take the state and an action and produce [`ReduceResult`](https://github.com/toggl/komposable-architecture/blob/main/komposable-architecture/src/main/java/com/toggl/komposable/architecture/ReduceResult.kt) which contains a new state and an optional effect.\n- **Effects** As mentioned, Reducers optionally produce these after handling an action. They are classes that return an optional action. All the effects emitted from a reducer will be batched, meaning the state change will only be emitted once all actions are handled.\n- **Subscriptions** Subscriptions emit actions based on some underlying observable API and/or state changes.   \n\nThere's one global `Store` and one `AppState`. But we can *view* into the store to get sub-stores that only work on one part of the state. More on that later.\n\nThere's also one main `Reducer` and multiple sub-reducers that handle a limited set of actions and only a part of the state. Those reducers are then *pulled back* and *combined* into the main reducer.\n\n## 🔎 Getting into the weeds\n\n### Store \u0026 State\n\nThe `Store` exposes a flow which emits the whole state of the app every time there's a change and a method to send actions that will modify that state.  The `State` is just a data class that contains ALL the state of the application. It also includes the local state of all the specific modules that need local state. More on this later.\n\nThe store interface looks like this:\n\n```kotlin\ninterface Store\u003cState, Action : Any\u003e {\n    val state: Flow\u003cState\u003e\n    fun send(actions: List\u003cAction\u003e)\n    // more code\n}\n```\n\nAnd you can create a new store using:\n\n```kotlin\ncreateStore(\n    initialState = AppState(),\n    reducer = reducer,\n    subscription = subscription,\n    dispatcherProvider = dispatcherProvider,\n    storeScopeProvider = application as StoreScopeProvider\n)\n```\n\nactions are sent like this:\n\n```kotlin\nstore.send(AppAction.BackPressed)\n```\n\nand views can subscribe like this:\n\n```kotlin\nstore.state\n    .onEach { Log.d(tag, \"The whole state: \\($0)\") }\n    .launchIn(scope)\n\n// or\n\nstore.state\n    .map { it.email }\n    .onEach { emailTextField.text = it }\n    .launchIn(scope)\n```\n\nThe store can be \"viewed into\", which means that we'll treat a generic store as if it was a more specific one which deals with only part of the app state and a subset of the actions. More on the Store Views section.\n\n### Actions\n\nActions are sealed classes, which makes it easier to discover which actions are available and also add the certainty that we are handling all of them in reducers.\n\n```kotlin\nsealed class EditAction {\n    data class TitleChanged(val title: String) : EditAction()\n    data class DescriptionChanged(val description: String) : EditAction()\n    data object CloseTapped : EditAction()\n    data object SaveTapped : EditAction()\n    data object Saved : EditAction()\n}\n```\n\nThese sealed actions are embedded into each other starting with the \"root\" `AppAction`\n\n```kotlin\nsealed class AppAction {\n    class List(override val action: ListAction) : AppAction(), ActionWrapper\u003cListAction\u003e\n    class Edit(override val action: EditAction) : AppAction(), ActionWrapper\u003cEditAction\u003e\n    data object BackPressed : AppAction()\n}\n```\n\nSo to send an `EditAction` to a store that takes `AppActions` we would do\n\n```kotlin\nstore.send(AppAction.Edit(EditAction.TitleChanged(\"new title\")))\n```\n\nBut if the store is a view that takes `EditAction`s we'd do it like this:\n\n```kotlin\nstore.send(EditAction.TitleChanged(\"new title\"))\n```\n\n### Reducers \u0026 Effects\n\nReducers are classes that implement the following interface:\n\n```kotlin\nfun interface Reducer\u003cState, Action\u003e {\n    fun reduce(state: State, action: Action): ReduceResult\u003cState, Action\u003e\n}\n\ndata class ReduceResult\u003cout State, Action\u003e(\n    val state: State,\n    val effect: Effect\u003cAction\u003e,\n)\n```\n\nThe idea is they take the previous state and an action and return the newly computed state as the first part of `ReduceResult\u003cState, Action\u003e`\n\nIn order to send actions asynchronously we use `Effect`s which are merged and sent as the second part of the `ReduceResult\u003cState, Action\u003e`. The store waits for those effects and sends whatever action they emit, if any.\n\nAn effect interface is also straightforward:\n\n```kotlin\nfun interface Effect\u003cout Action\u003e {\n    fun run(): Flow\u003cAction\u003e\n    // more code\n}\n```\n\n### Subscriptions\nSubscriptions are similar to effects:\n\n```kotlin\nfun interface Subscription\u003cState, Action : Any\u003e {\n    fun subscribe(state: Flow\u003cState\u003e): Flow\u003cAction\u003e\n}\n```\n\nThe difference is that Subscriptions are not triggered by Actions. They start immediately after the store is created and continue emitting as long as the store exists. \n\nSubscriptions are typically used to observe some data in the database:\n\n```kotlin\nclass ListSubscription @Inject constructor(val todoDao: TodoDao) : Subscription\u003cAppState, AppAction\u003e {\n    override fun subscribe(state: Flow\u003cAppState\u003e): Flow\u003cAppAction\u003e =\n        todoDao.getAll().map { AppAction.List(ListAction.ListUpdated(it)) }\n}\n```\n\nOr some other observable APIs like for example location services. Subscription flow can be also steered by state changes:\n\n```kotlin\nclass ListSubscription @Inject constructor(val locationProvider: LocationProvider) : Subscription\u003cAppState, AppAction\u003e {\n    override fun subscribe(state: Flow\u003cAppState\u003e): Flow\u003cAppAction\u003e =\n        if (state.isPermissionGranted) \n          locationProvider.observeCurrentLocation().map { AppAction.Map(MapAction.LocationUpdated(it)) }\n        else \n          flowOf()\n}\n```\n\n\n### Pullback\n\nThere's one app level reducer that gets injected into the store. This reducer takes the whole `AppState` and the complete set of `AppActions`. \n\nThe rest of the reducers only handle one part of that state, for a particular subset of the actions.\n\nThis aids in modularity. But in order to merge those reducers with the app level one, their types need to be compatible. That's what `pullback` is for. It converts a specific reducer into a global one.\n\n```kotlin\ninternal class PullbackReducer\u003cLocalState, GlobalState, LocalAction, GlobalAction\u003e(\n    private val innerReducer: Reducer\u003cLocalState, LocalAction\u003e,\n    private val mapToLocalState: (GlobalState) -\u003e LocalState,\n    private val mapToLocalAction: (GlobalAction) -\u003e LocalAction?,\n    private val mapToGlobalState: (GlobalState, LocalState) -\u003e GlobalState,\n    private val mapToGlobalAction: (LocalAction) -\u003e GlobalAction,\n) : Reducer\u003cGlobalState, GlobalAction\u003e {\n    override fun reduce(\n        state: GlobalState,\n        action: GlobalAction,\n    ): ReduceResult\u003cGlobalState, GlobalAction\u003e {\n        val localAction = mapToLocalAction(action)\n            ?: return ReduceResult(state, NoEffect)\n\n        val localResult = innerReducer.reduce(mapToLocalState(state), localAction)\n\n        return ReduceResult(\n            mapToGlobalState(state, localResult.state),\n            localResult.effect.map(mapToGlobalAction),\n        )\n    }\n}\n```\n\nAfter we've transformed the reducer we can use `combine` to merge it with other reducers to create one single reducer that is then injected into the store.\n\n### Store Views\n\nSimilarly to reducers and pullback, the store itself can be \"mapped\" into a specific type of store that only holds some part of the state and only handles some subset of actions. Only this operation is not exactly \"map\", so it's called `view`.\n\n```kotlin\nclass MutableStateFlowStore\u003cState, Action : Any\u003e private constructor(\n    override val state: Flow\u003cState\u003e,\n    private val sendFn: (List\u003cAction\u003e) -\u003e Unit\n) : Store\u003cState, Action\u003e {\n\n    override fun \u003cViewState, ViewAction : Any\u003e view(\n        mapToLocalState: (State) -\u003e ViewState,\n        mapToGlobalAction: (ViewAction) -\u003e Action?,\n    ): Store\u003cViewState, ViewAction\u003e = MutableStateFlowStore(\n        state = state.map { mapToLocalState(it) }.distinctUntilChanged(),\n        sendFn = { actions -\u003e\n            val globalActions = actions.mapNotNull(mapToGlobalAction)\n            sendFn(globalActions)\n        },\n    )\n}\n```\n\nThis method on `Store` takes two functions, one to map the global state into local state and another one to map local action to global action.\n\nDifferent modules or features of the app use different store views so they are only able to listen to changes to parts of the state and are only able to send certain actions.\n\n### Local State\n\nSome features have the need of adding some state to be handled by their reducer, but maybe that state is not necessary for the rest of the application. Consider email \u0026 password fields in a theoretical Auth module.\n\nTo deal with this kind of state we do the following:\n- In the module's state use a public class with internal properties to store the needed local state\n- We store that property in the global state. So that state in the end is part of the global state and it behaves the same way, but can only be accessed from the module that needs it.\n\nThis is how could the AuthState look like:\n```kotlin\ndata class AuthState(\n    val user: Loadable\u003cUser\u003e,\n    val localState: LocalState\n) {\n    data class LocalState internal constructor(\n        internal val email: Email,\n        internal val password: Password\n    ) {\n        constructor() : this(Email.Invalid(\"\"), Password.Invalid(\"\"))\n    }\n}\n```\n\nThis is how it looks in the global app state\n```kotlin\ndata class AppState(\n    val authLocalState: AuthState.LocalState = AuthState.LocalState(),\n)\n```\n\n### High-order reducers\n\nHigh-order reducers are basically reducers that take another reducer (and maybe also some other parameters). The outer reducer adds some behavior to the inner one, maybe transforming actions, stopping them or doing something with them before sending them forward to the inner reducer.\n\nThe simplest example of this is a logging reducer, which logs every action sent to the console:\n\n\n```kotlin\nclass LoggingReducer(override val innerReducer: Reducer\u003cAppState, AppAction\u003e)\n    : HigherOrderReducer\u003cAppState, AppAction\u003e {\n    override fun reduce(\n        state: AppState,\n        action: AppAction\n    ): ReduceResult\u003cAppAction\u003e {\n        Log.i(\n            \"LoggingReducer\", when (action) {\n                is AppAction.List -\u003e action.list.formatForDebug()\n                is AppAction.Edit -\u003e action.edit.formatForDebug()\n            }\n        )\n\n        return innerReducer.reduce(state, action)\n    }\n}\n```\n\n## ✅ Testing Extensions \n\nIf you decide to include `com.toggl:komposable-architecture-test` to your dependencies, you'll be able to use a small set of Reducer extensions designed to make testing easier.\n\nTake a look at this test from [Todo Sample](https://github.com/toggl/komposable-architecture/tree/main/todo-sample) app which is making a good use of `testReduce` extension method:\n\n```kotlin\n@Test\nfun `ListUpdated action should update the list of todos and return no effects`() = runTest {\n     val initialState = ListState(todoList = emptyList(), backStack = emptyList())\n     reducer.testReduce(\n         initialState,\n         ListAction.ListUpdated(listOf(testTodoItem))\n     ) { state, effect -\u003e\n         assertEquals(initialState.copy(todoList = listOf(testTodoItem)), state)\n         assertEquals(NoEffect, effect)\n     }\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftoggl%2Fkomposable-architecture","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftoggl%2Fkomposable-architecture","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftoggl%2Fkomposable-architecture/lists"}