{"id":16464967,"url":"https://github.com/sellmair/evas","last_synced_at":"2025-04-05T12:02:03.291Z","repository":{"id":252448572,"uuid":"827882526","full_name":"sellmair/evas","owner":"sellmair","description":"Modern, high performance, tiny EventBus and State Container in Kotlin","archived":false,"fork":false,"pushed_at":"2025-03-20T09:48:56.000Z","size":3267,"stargazers_count":158,"open_issues_count":1,"forks_count":3,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-03-29T11:03:33.831Z","etag":null,"topics":["compose","eventbus","events","kmp","kotlin","kotlinmultiplatform","state-management","states"],"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/sellmair.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":"2024-07-12T15:28:37.000Z","updated_at":"2025-03-27T16:41:21.000Z","dependencies_parsed_at":"2024-10-17T20:21:43.870Z","dependency_job_id":"68801202-100e-4330-a593-5c59479f6a01","html_url":"https://github.com/sellmair/evas","commit_stats":null,"previous_names":["sellmair/evas"],"tags_count":13,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sellmair%2Fevas","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sellmair%2Fevas/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sellmair%2Fevas/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sellmair%2Fevas/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sellmair","download_url":"https://codeload.github.com/sellmair/evas/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247332556,"owners_count":20921853,"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":["compose","eventbus","events","kmp","kotlin","kotlinmultiplatform","state-management","states"],"created_at":"2024-10-11T11:30:57.452Z","updated_at":"2025-04-05T12:02:03.273Z","avatar_url":"https://github.com/sellmair.png","language":"Kotlin","funding_links":[],"categories":["Libraries","进程间通信"],"sub_categories":["🏗 Architecture","Spring Cloud框架"],"readme":"# **Ev**ents **a**nd **S**tates for Kotlin\n\n\u003cp\u003e\n\u003cimg src=\".img/logo_signed.png\" width=\"365\"\nalt=\"Evas logo by Sebastian Sellmair\"\u003e\n\u003c/p\u003e\n\n# What is Evas?\n\nEvas is a **library** providing\n\n## `Events`\nA performant, scalable and most importantly **scoped** Event-Channel integrated with coroutines (and compose), which\noffers advantages like a traditional \"Event Bus\" (such as flexibility, decoupling, simplicity), whilst retaining\nstructured concurrency, clear scoping, and easy testing.\n\n## `States`\nA pragmatic state container, offering the accessibility and ease of use of a Singleton, whilst also retaining clearly\nstructured concurrency, scoping, and easy testing.\n\n---\nDiscussions about architecture are fun, but at its core usually evolve around answering the following questions\n\n1. Where does my 'State' live? -\u003e `States`\n2. How to pass 'Events' around? -\u003e `Events`\n3. How are Events and States connected? -\u003e `kotlinx.coroutines`\n\n___ \n\n✅ Multiplatform (jvm, android, iOS, watchOS, macOS, linux, windows, wasm, js, ...)\u003cbr\u003e\n✅ Fast / Performance benchmarked (kotlinx.benchmark)\u003cbr\u003e\n✅ Concurrency tested (kotlinx.lincheck)\u003cbr\u003e\n✅ API stability tested (kotlinx.binary-compatibility-validator)\u003cbr\u003e\n✅ Tiny Binary Size (~ 90kb)\u003cbr\u003e\n➕ Compose Extensions\u003cbr\u003e\n➕ Inline documentation with 'usage examples'\u003cbr\u003e\n\n---\n\n## Dependencies\n\n```kotlin\nimplementation(\"io.sellmair:evas:1.2.0\")\n```\n\nMaven:\n```xml\n\u003cdependency\u003e\n    \u003cgroupId\u003eio.sellmair\u003c/groupId\u003e\n    \u003cartifactId\u003eevas-jvm\u003c/artifactId\u003e\n    \u003cversion\u003e1.2.0\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\n(Compose Extensions)\n\n```kotlin\nimplementation(\"io.sellmair:evas-compose:1.2.0\")\n```\n\n\n---\n\n# Simple Usage\n\n## Setup\n\nInstances of the `Events` (Event Bus) and `States`(State Container) can simply be created using the\n`Events()` and `States()` factory functions.\n\n### Coroutines Context\n\nBinding them to the current coroutine context is as simple as\n\n[snippet]: (setup-coroutines.kt)\n```kotlin\nval events = Events() // \u003c- create new instance\nval states = States() // \u003c- create new instance\nwithContext(events + states) {\n\n}\n```\n\n### Compose Extensions\n\nBinding the event bus or state container to compose is as simple as\n\n[snippet]: (setup-compose.kt)\n```kotlin\nval events = Events() // \u003c- create new instance\nval states = States() // \u003c- create new instance\n\n@Composable\nfun App() {\n    installEvas(events, states) {\n        MainPage()\n    }\n}\n```\n\n## Send and Subscribe to Events\n\nThe following snippet shows how two Events are processed:\n\n1) `LoginEvent`: Will be fired once a user successfully logged into our application\n2) `LogoutEvent`: Will be fired once a user intends to log out\n\nOnce the `Events` instance is installed in the current 'Coroutine Context', listening for them\ncan be done usinge the `collectEvents` method.\n\nFiring an event can simply be done by calling `emit`:\nNote: The `emit()` function will suspend until all listening coroutines finished processing.\n(See `emitAsync()` to dispatch events without waiting for all listeners.\n\n[snippet]: (loginEvents.kt)\n```kotlin\nobject LogoutEvent: Event\n\ndata class LoginEvent(val userName: String, val token: String): Event\n\n/**\n * Use 'collectEvents' to subscribe to all events of type [LogoutEvent]\n */\nsuspend fun listenForLogout() = collectEvents\u003cLogoutEvent\u003e {\n    println(\"User logged out\")\n}\n\n/**\n * Use 'collectEvents' to subscribe to all events of type [LoginEvent]\n */\nsuspend fun listenForLogin() = collectEvents\u003cLoginEvent\u003e { event -\u003e\n    println(\"User: ${event.userName} logged in\")\n}\n\n/**\n * Example function 'login' which will pretend to login a user and then\n * emits a [LoginEvent]\n */\nsuspend fun login(userName: String, password: String) {\n    val token = httpClient().login(userName, password) ?: return\n    LoginEvent(userName, token).emit()\n                    //          ^\n                    // emit the event and suspend until\n                    // All listeners have finished processing this event\n}\n\n/**\n * Example function 'logout' which will pretend to delete user data and then\n * emits a [LogoutEvent]\n */\nsuspend fun logout() {\n    deleteUserData()\n    LogoutEvent.emit()\n    //          ^\n    // emit the event and suspend until\n    // All listeners have finished processing this event\n}\n```\n\n## Simple State\n\nDefining a simple State counting the number of 'clicks' performed by a user\n\n[snippet]: (simpleClickCounterState.kt)\n```kotlin\n/**\n * Pretend: Our application cares about the number of 'clicks' the user has performed.\n * We define our [ClickCounterState] here which holds this information.\n */\ndata class ClickCounterState(val count: Int) : State {\n    /*\n    Using the 'companion object' as Key, defining '0' as the default state\n     */\n    companion object Key : State.Key\u003cClickCounterState\u003e {\n        override val default: ClickCounterState = ClickCounterState(count = 0)\n    }\n}\n\n/**\n * Launching the coroutine, which will produce the [ClickCounterState].\n * It will collect the [ClickEvent]s and update the [ClickCounterState] by incrementing for each click it receives.\n */\nfun CoroutineScope.launchClickCounterState() = launchState(ClickCounterState) {\n    var count = 0\n    collectEvents\u003cClickEvent\u003e {\n        count++\n        ClickCounterState(count).emit()\n        //                        ^\n        //                 Emit State Update\n    }\n}\n\n/**\n * Imaginary 'onClick' method which will send the [ClickEvent] to the application\n */\nsuspend fun onClick() {\n    ClickEvent.emit()\n    //          ^\n    // emit event and wait for all listening coroutines to finish\n}\n```\n\nUsing this state and print updates to the console\n[snippet]: (usingClickCounterState.kt)\n```kotlin\nfun CoroutineScope.launchClickCounterPrinter() = launch {\n    ClickCounterState.collect { state -\u003e\n        println(\"Click Count: ${state.count}\")\n    }\n}\n```\n\n## Launch State Producer \u0026 Show Compose UI\n\n### Define a 'hot' state and 'hot' state producer\nIn this example we're going to model the 'Login State' of a user which can be\na) Logged Out\nb) Currently Logging In\nc) Logged In\n\nFor this the State can be modeled using a sealed class.\n\nThe state will be produced by a 'launchState' coroutine, which will try to find the user data\nfrom a local database and handles login requests (sent as events)\n\n[snippet]: (hotUserLoginState.kt)\n ```kotlin\n\n/**\n * Defining an imaginary [UserLoginState] which knows about if the User is currently logged into our\n * application (or currently attempting to)\n *\n * In this example 'null' is chosen as the default state, representing that it is \"unknown\"\n */\nsealed class UserLoginState : State {\n    companion object Key : State.Key\u003cUserLoginState?\u003e {\n        override val default: UserLoginState? = null\n    }\n\n    data object LoggedOut : UserLoginState()\n    data object LoggingIn : UserLoginState()\n    data class LoggedIn(val userId: UserId) : UserLoginState()\n}\n\n/**\n * Launching the [UserLoginState] producing coroutine:\n * This coroutine will:\n * - Try to find the currently logged-in user data from a local database\n * - Handle [LoginRequest] events and tries to log a user in, if received\n */\nfun CoroutineScope.launchUserLoginState() = launchState(UserLoginState) {\n    val user = getUserFromDatabase()\n    if (user != null) {\n        LoggedIn(user.userId).emit()\n        return@launchState\n    }\n\n    /**\n     * Oh, oh: User wasn't found in the local database:\n     * We're setting the state to [LoggedOut]\n     */\n    LoggedOut.emit()\n\n    /**\n     * From here on, we collect all [LoginRequest] events and try to log the user in, by hitting\n     * the network.\n     */\n    collectEvents\u003cLoginRequest\u003e { request -\u003e\n        LoggingIn.emit()\n\n        val response = sendLoginRequestToServer(request.user, request.password)\n        if (response.isSuccess) {\n            LoggedIn(response.userId).emit()\n        } else {\n            LoggedOut.emit()\n        }\n    }\n}\n ```\n\n### Use State in UI development (e.g., compose, using `io.sellmair:evas-compose`)\n\n[snippet]: (composeValue.kt)\n ```kotlin\n@Composable\nfun App() {\n    val loginState = UserLoginState.composeValue()\n    //                                   ^\n    //         Will trigger re-composition if the state changes\n\n    when (loginState) {\n        is LoggedOut -\u003e ShowLoginScreen()\n        is LoggingIn -\u003e ShowLoginSpinner()\n        is LoggedIn -\u003e ShowMainScreen()\n        null -\u003e Unit\n    }\n}\n ```\n\n___\n\n# Sample Projects\n\n## Login Screen App (iOS, Android, Desktop App)\n\n- [Entry Point: Android](samples/login-screen/src/androidMain/kotlin/io/sellmair/sample/MainActivity.kt)\n- [Entry Point: iOS]()\n\n## Joke App (iOS, Android, Desktop App)\n\n- [Entry Point: Android](samples/joke-app/src/androidMain/kotlin/io/sellmair/jokes/MainActivity.kt)\n- [Entry Point: iOS](samples/login-screen/src/iosMain/kotlin/io/sellmair/sample/SampleAppViewController.kt)\n- [Entry Point: Desktop](samples/login-screen/src/jvmMain/kotlin/io/sellmair/sample/SampleApplication.kt)\n- [Login Screen: Compose UI](samples/login-screen/src/commonMain/kotlin/io/sellmair/sample/ui/LoginScreen.kt)\n\n## CLI Application (Directory Scanner)\n\n![directory-statistics-cli.gif](samples/directory-statistics-cli/.img/directory-statistics-cli.gif)\n\n- [Entry Point: Main.kt](https://github.com/sellmair/evas/blob/895fcb39528ff008bcbbe5959b3f79298caabbdc/samples/directory-statistics-cli/src/nativeMain/kotlin/Main.kt)\n- [SummaryState](samples/directory-statistics-cli/src/nativeMain/kotlin/SummaryState.kt)\n- [Command Line UI](samples/directory-statistics-cli/src/nativeMain/kotlin/uiActor.kt)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsellmair%2Fevas","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsellmair%2Fevas","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsellmair%2Fevas/lists"}