{"id":19444189,"url":"https://github.com/fededri/arch","last_synced_at":"2025-04-25T00:33:01.238Z","repository":{"id":57733192,"uuid":"323717331","full_name":"fededri/Arch","owner":"fededri","description":"KMM library to architecture your app following functional concepts, state management, ViewModels and coroutines","archived":false,"fork":false,"pushed_at":"2022-08-15T08:57:04.000Z","size":2354,"stargazers_count":53,"open_issues_count":2,"forks_count":2,"subscribers_count":2,"default_branch":"kmm-arch","last_synced_at":"2025-04-03T14:21:28.709Z","etag":null,"topics":["android","coroutines","kotlin-android","state-management"],"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/fededri.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}},"created_at":"2020-12-22T19:39:30.000Z","updated_at":"2023-09-22T20:09:51.000Z","dependencies_parsed_at":"2022-09-26T22:30:42.738Z","dependency_job_id":null,"html_url":"https://github.com/fededri/Arch","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/fededri%2FArch","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fededri%2FArch/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fededri%2FArch/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fededri%2FArch/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fededri","download_url":"https://codeload.github.com/fededri/Arch/tar.gz/refs/heads/kmm-arch","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250733606,"owners_count":21478400,"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","coroutines","kotlin-android","state-management"],"created_at":"2024-11-10T16:05:29.012Z","updated_at":"2025-04-25T00:33:00.829Z","avatar_url":"https://github.com/fededri.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"![Arch](logo.png)\nArch is a small and lightweight Kotlin multiplatform library that helps to architecture Mobile Applications (iOS/Android), it is based on several concepts of the functional paradigm and  Spotify's Mobius library but instead of using RxJava, it uses **Coroutines, SharedFlow and StateFlow**\n\nFor Android targets, this library is built upon the Android's ViewModel class and takes full advantage of it\n\n\n## Download\nArch is uploaded to maven central, to start using it on your KMM project add these dependencies\n```kotlin\nval commonMain by getting {\n            dependencies {\n                api(\"io.github.fededri.arch:shared:0.5\")\n            }\n        }\n        \n val iosMain by getting {\n            dependencies {\n                implementation(\"io.github.fededri.arch:shared-ios:0.5\")\n            }\n        }\n```\n\n## ArchViewModel\nIt is an abstract class extending from *ViewModel* and contains the main logic of this library,\n\n## State\nHandling state properly in every application is critical, with **Arch** you must define a custom State data class for each of your ViewModels, this state will be **inmmutable**\n\n## RenderState\nOptionally you can make a distinction between domain-state from render-state, so your views only has access to the data and objects they need to render the screen, in order to do that you have to pass into ArchViewModel's constructor a `StateMapper`\n\n## Actions\nWith **Arch** the only way of changing anything in the app is by dispatching an **Action**, usually actions will be dispatched by user interactions, but not always.\n\n## Updater\nUpdater is an interface with just one **pure function**, it receives an action, creates a new state from the previous one and returns a `Next` object. \n`Next` is a sealed class with types:\n- `Next.State`\n- `Next.StateWithSideEffects`\n- `Next.StateWithSideEffectsAndEvents`\n- `Next.StateWithEvents`\n\nDepending on what type of `Next` object the ***Updater** returns in addition to changing the state, you can dispatch **SideEffects** and **Events** \n\n## SideEffects\nThey are usually blocking or long running operations like database transactions or network requests. When a SideEffect is dispatched, a new coroutine is created in order to execute it. By default the coroutine will be cancelled if the ViewModel is cleared, but you can pass in a custom `CoroutineScope` if you want to keep alive the coroutine even after the ViewModel is cleared, my recommendation -if that is the case - is to create a new `CoroutineScope` inside your `Application` class and pass it to the SideEffect's constructor\n\n \n## Events\nUsually they represent something that you want to notify to your view layer like popups, animations and transitions. If you have a single-activity architecture with multiple fragments, you can observe the same events from several fragments.\n\nIn your ViewModel's constructor, you can pass into the parameters an ``EventsConfiguration``, it is not mandatory but passing this parameter allows you to configure yours events.\n- `Replays`: the number of events replayed to new subscribers, default is zero\n- `ExtraBufferCapacity`: the number of values buffered in addition to `replay`, events under the hood are backed by a `SharedFlow` so if you emit an event and there is no space remaining in your buffer then the collector will be suspended\n- `OnBufferOverflow` you can configure what action to take in case of buffer overflow, the default behavior is suspending\n\n## FlowWrapper\nIt is an implementation of a `Flow`, but it adds a method so iOS clients can collect the flow.\n```kotlin\nfun collect(onEach: (T) -\u003e Unit, onCompletion: (cause: Throwable?) -\u003e Unit): Cancellable {\n...\n}\n```\n\n## Basic Usage\n[Here](app/src/main/java/com/fedetto/example/) is an example of a basic usage of the library, this example is a counter with two buttons: up and down. When the counter reaches a multiple of ten, a ´SideEffect´ is dispatched that simulates an Input/Output operation and resets the counter.\n\n#### What we should do first is define our `Actions` , `State`, and `SideEffects`\n\n```kotlin\ndata class State(val counter: Int = 0)\n\nsealed class Action {\n    object Up : Action()\n    object Down : Action()\n    object Reset : Action()\n}\n\nsealed class Event {\n    data class LogSomething(val text: String) : Event()\n}\n\nsealed class SideEffect(\n    //Use IO dispatcher to run side effects\n    override val dispatcher: CoroutineDispatcher = Dispatchers.IO,\n    //use viewModelScope\n    override val coroutineScope: CoroutineScope? = null\n) : SideEffectInterface {\n    //Set counter to zero\n    object ResetEffect : SideEffect()\n}\n```\n\n#### Now we need our `Processor` and `Updater`\n```kotlin\nclass CounterProcessor : Processor\u003cSideEffect, Action\u003e {\n\n    override suspend fun dispatchSideEffect(effect: SideEffect): Action {\n        delay(3000)\n        return Action.Reset\n    }\n}\n\nclass CounterUpdater : Updater\u003cAction, State, SideEffect, Event\u003e {\n\n    override fun onNewAction(action: Action, currentState: State): Next\u003cState, SideEffect, Event\u003e {\n        return when (action) {\n            Action.Up -\u003e changeCounter(currentState, true)\n            Action.Down -\u003e changeCounter(currentState, false)\n            Action.Reset -\u003e Next.State(currentState.copy(counter = 0))\n        }\n    }\n\n    private fun changeCounter(\n        currentState: State,\n        isIncrement: Boolean\n    ): Next\u003cState, SideEffect, Event\u003e {\n        val next = when (isIncrement) {\n            true -\u003e currentState.counter + 1\n            else -\u003e currentState.counter - 1\n        }\n\n        return if ((next % 10) == 0) {\n            Next.StateWithSideEffectsAndEvents(\n                currentState.copy(counter = next),\n                setOf(SideEffect.ResetEffect),\n                setOf(Event.LogSomething(\"Multiple of 10\"))\n            )\n        } else {\n            Next.State(currentState.copy(counter = next))\n        }\n    }\n}\n```\n\n\n#### Finally we just need to create our ViewModel, dispatch actions and observe the state from our iOS or Android views\n\n\n```kotlin\nclass ViewModel(\n    updater: Updater\u003cAction, State, SideEffect, Event\u003e,\n    processor: Processor\u003cSideEffect, Action\u003e\n) : ArchViewModel\u003cAction, State, SideEffect, Event, Nothing\u003e(\n    updater,\n    State(),\n    //Here we dispatch an initial SideEffect\n    setOf(SideEffect.ResetEffect),\n    processor\n)\n\n\nclass MainActivity : AppCompatActivity() {\n\n    lateinit var viewModel: ViewModel\n    private val textView by lazy { findViewById\u003cTextView\u003e(R.id.textView) }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContentView(R.layout.activity_main)\n\n        viewModel = ViewModel(Updater(), CounterProcessor())\n        lifecycleScope.launchWhenResumed {\n            viewModel.observeState().collect {\n                renderState(it)\n            }\n        }\n\n        findViewById\u003cButton\u003e(R.id.up).setOnClickListener {\n            viewModel.action(Action.Up)\n        }\n\n        findViewById\u003cButton\u003e(R.id.down).setOnClickListener {\n            viewModel.action(Action.Down)\n        }\n    }\n\n    private fun renderState(state: State) {\n        textView.text = state.counter.toString()\n    }\n}\n        \n        \n```\n\n#### Another example\nI have migrated one of my demo apps to Arch, [This one](https://github.com/fededri/Reddit_Client/tree/coroutines_version) is a bit more complex than the previous example and fetchs real data from Reddit's API and makes use of `RenderState`, `Events` and error handling\n\n## Error Handling\nSideEffects execution is accomplished by using coroutines, so the error handling is very similar, if one effect's coroutine throws an exception and you don't catch it inside your code the app will throw the exception.\n\nIn that case, if  you want to avoid a crash, you can catch all coroutine exceptions defining a custom `CoroutineExceptionHandler` and pass it into ArchViewModel's constructor\n\n```kotlin\nclass EffectsExceptionHandler: AbstractCoroutineContextElement(CoroutineExceptionHandler),\n    CoroutineExceptionHandler {\n    override fun handleException(context: CoroutineContext, exception: Throwable) {\n        //handle your exception\n    }\n}\n```\n\nIf you are using the default `CoroutineScope` (viewModelScope), children coroutines fail independently of each other because this scope uses a `SupervisorJob`, but if you set a custom scope you must take care of this case\n\n## Running SideEffects that lives outside of your ViewModel lifecycle\nAll SideEffects are cancelled when your ViewModel is cleared. If you want to avoid this, specify a custom scope so you can dispatch long-running effects independent of your ViewModel lifecycle. You can specify in which thread and scope you want to run each of your SideEffects, I recommend creating a new scope in your `Application` class, just take in mind that you have to take care of cancellation and exceptions.\n\n\n```kotlin\nsealed class SideEffect(\n    //Use CPU dispatcher to run side effects\n    override val dispatcher: CoroutineDispatcher = Dispatchers.IO,\n    //use viewModelScope\n    override val coroutineScope: CoroutineScope? = null\n) : SideEffectInterface {\n    \n    //This effect will run in a custom scope and will use the Default dispatcher\n    object ResetEffect : SideEffect(Dispatchers.Default, CustomCoroutineScope())\n}\n```\n\n\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffededri%2Farch","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffededri%2Farch","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffededri%2Farch/lists"}