{"id":13537366,"url":"https://github.com/beyondeye/compose_bloc","last_synced_at":"2025-08-30T17:19:11.692Z","repository":{"id":57832851,"uuid":"509066769","full_name":"beyondeye/compose_bloc","owner":"beyondeye","description":"Kotlin Multiplatform State Management and Navigation Library for Compose","archived":false,"fork":false,"pushed_at":"2024-04-07T08:00:36.000Z","size":720,"stargazers_count":16,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-02T04:35:13.937Z","etag":null,"topics":["android","android-library","android-navigation","bloc","compose","compose-multiplatform","event-driven","kotlin","kotlin-android","kotlin-multiplatform","mvi","navigation","navigator","reactive","router","state-management","streams"],"latest_commit_sha":null,"homepage":"https://beyondeye.gitbook.io/compose-bloc/","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/beyondeye.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","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":"2022-06-30T12:15:09.000Z","updated_at":"2024-06-05T09:40:48.000Z","dependencies_parsed_at":"2024-11-03T02:31:30.550Z","dependency_job_id":null,"html_url":"https://github.com/beyondeye/compose_bloc","commit_stats":null,"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"purl":"pkg:github/beyondeye/compose_bloc","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beyondeye%2Fcompose_bloc","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beyondeye%2Fcompose_bloc/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beyondeye%2Fcompose_bloc/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beyondeye%2Fcompose_bloc/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/beyondeye","download_url":"https://codeload.github.com/beyondeye/compose_bloc/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beyondeye%2Fcompose_bloc/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":272878341,"owners_count":25008342,"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","status":"online","status_checked_at":"2025-08-30T02:00:09.474Z","response_time":77,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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","android-library","android-navigation","bloc","compose","compose-multiplatform","event-driven","kotlin","kotlin-android","kotlin-multiplatform","mvi","navigation","navigator","reactive","router","state-management","streams"],"created_at":"2024-08-01T09:00:58.144Z","updated_at":"2025-08-30T17:19:11.666Z","avatar_url":"https://github.com/beyondeye.png","language":"Kotlin","readme":"![Kotlin version](https://img.shields.io/static/v1?label=Kotlin\u0026message=1.7.10\u0026color=Orange\u0026style=for-the-badge)\n[![Maven Central](https://img.shields.io/maven-central/v/io.github.beyondeye/kbloc-navigator?style=for-the-badge)](https://search.maven.org/artifact/io.github.beyondeye/kbloc-navigator)\n![Release](https://img.shields.io/github/v/release/beyondeye/compose_bloc?style=for-the-badge)\n![Issues](https://img.shields.io/github/issues/beyondeye/compose_bloc?style=for-the-badge)\n![License Apache 2.0](https://img.shields.io/github/license/beyondeye/compose_bloc?style=for-the-badge)\n# What is it\nA port for Compose of [flutter bloc](https://github.com/felangel/bloc) for better state management \nintegrated with a navigation library\n(a fork of [voyager](https://github.com/adrielcafe/voyager) library)\n\n# Setup\nAdd Maven Central to your repositories if needed\n```groovy\nrepositories {\n    mavenCentral()\n}\n```\nAdd the main library dependency to your module `build.gradle`.\n```groovy\n    // Bloc+Navigator core library \n    implementation \"io.github.beyondeye:kbloc-navigator:$version\"\n```\nFor available  versions look at  [compose_bloc releases](https://github.com/beyondeye/compose_bloc/releases)\nthe library is a multiplatform library that support  Android and Desktop and Web compose.\n\n# Documentation\n- [Navigator Overview](https://beyondeye.gitbook.io/compose-bloc/navigator-documentation/navigator-overview)\n- [Router and path-based navigation](https://beyondeye.gitbook.io/compose-bloc/router-and-path-based-navigation)\n- [Bloc and Cubit Overview](https://beyondeye.gitbook.io/compose-bloc/bloc-documentation/bloc-and-cubit-overview)\n- [Blocs and Compose Overview](https://beyondeye.gitbook.io/compose-bloc/bloc-documentation/blocs-and-compose-overview)\n# Show me some code\n```kotlin\nclass MainActivity : ComponentActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        //standard setContent to define UI for an activity with Compose\n        setContent {\n            //use RootNavigator to initialize compose_bloc navigator for activity\n            RootNavigator(MainScreen(userName=\"Albert Einstein\"))\n        }\n    }\n}\n\n// A Screen is what the name suggests, and is the basic UI entity to which you can\n// navigate to with a Navigator. Entities associated with a Screen like a ScreenModel\n// or Bloc are automatically disposed when a Screen is disposed, for example when we\n// receive an \"onBackPressed\" event in a Screen.\n// Note that a Screen must be Serializable, so that its fields, that you can think as \n// it \"arguments\" can be saved and restored when an Activity is paused and then restarted\n// or when screen is rotated.\nclass MainScreen(val userName:String): Screen {\n    @Composable\n    override fun Content() {\n        // the current navigator, in this case it is the root navigator.\n        // It is possible to define nested navigators\n        val navigator= LocalNavigator.currentOrThrow\n        Compose_blocTheme {\n            Scaffold(\n                topBar = { TopAppBar{ Text(\"Counter test app\") } },\n                backgroundColor = MaterialTheme.colors.background,\n                    Column {\n                        Text(\"Hello $userName\")\n                        // when the user click the button we navigate to a new screen\n                        Button(onClick={ navigator.push(CounterScreen())})\n                             { Text(\"click to open Counter Screen\") }\n                    }\n                }\n            )\n        }\n    }\n}\n\n// The idea behind the Bloc architecture, very similar to Redux, is that we\n// don't modify directly the state of the application (the state associated to the Bloc)\n// but instead we send events (\"actions\" in Redux) that are processed\n// by the Bloc registered event handlers \n\n// Here is the definition of the events CounterBloc can handle, all events\n// must inherit to some base event type that the bloc is supposed to handle\ninterface CounterEvent\nclass AdditionEvent(val value:Int):CounterEvent\nclass SubtractionEvent(val value:Int):CounterEvent\n\n// this is the state of the bloc. It should be an immutable object, that we don't\n// update in-place. instead we create a new instance with the modified values.\n// a data class is perfect for this purpose\ndata class CounterState(val counter:Int=0)\n\n// here we define the Bloc itself. The bloc wrap together\n// - some application state (CounterState)\n// - associated event handlers to handle changes to the application state\n// - a kotlin Flow with the current value of the bloc state, that we can transform to\n//   compose MutableState and listen to, for  automatically updating the UI \n//   on state changes\nclass CounterBloc(cscope: CoroutineScope, startCounter:Int=0):\n Bloc\u003cCounterEvent, CounterState\u003e(\n     // every bloc has an associated coroutineScope that is automatically cancelled\n     // when the bloc is disposed.\n     cscope,   CounterState(startCounter),false) \n {\n    // in the bloc constructor we define the bloc event handlers\n    init {\n        // each event handler define a function that is called when\n        // an event of the specified type is received. the function\n        // has two arguments\n        // - the received event\n        // - an \"emit\" method to call to emit the updated state according to\n        //   the received event. \n        // Note that unlike Redux reducers, event handler are not necessarily pure\n        // function without side-effects. On the contrary there can be event handlers\n        // whose only purpose are their side effects, and that do not emit a new state\n        // at all. Use the coroutine scope associated to the bloc to run the side effects\n        on\u003cAdditionEvent\u003e { event, emit -\u003e\n            val s=state\n            emit(s.copy(counter =s.counter+event.value ))\n        }\n        on\u003cSubtractionEvent\u003e { event, emit -\u003e\n            val s=state\n            emit(s.copy(counter =s.counter-event.value ))\n        }\n    }\n}\n\nclass CounterScreen: Screen {\n    @Composable\n    override fun Content() {\n          Column(modifier=Modifier.fillMaxWidth(), \n                 horizontalAlignment = Alignment.CenterHorizontally) {\n            // out of the BlocProvider composable subtree the bloc is not available\n            val bnull= rememberProvidedBlocOf\u003cCounterBloc\u003e()\n            Log.e(LOGTAG,\"obtained bnull counter bloc: $bnull\")  //this must be null\n            // BlocProvider makes available the specified bloc (CounterBloc)\n            // to the associated composable subtree\n            BlocProvider(create = {cscope-\u003e CounterBloc(cscope,1)} ) {\n                //rememberProvidedBlocOf is similar to dependency injection:\n                //  it retrieves the specified bloc type as defined by the closest\n                //  enclosing BlocProvider\n                val b= rememberProvidedBlocOf\u003cCounterBloc\u003e()?:return@BlocProvider\n                // define some callbacks to wrap sending events to the bloc so\n                // that the actual UI does need to know anything about the bloc\n                val onIncrement = { b.add(AdditionEvent(1)) }\n                val onDecrement = { b.add(SubtractionEvent(1) }\n                //BlocBuilder search for the specified bloc type as defined by \n                // the closest enclosing blocProvider and subscribes to its states\n                // updates, as a Composable mutableState  that when changes trigger\n                // recomposition\n                BlocBuilder(b) { counterState-\u003e\n                    //this is the actual ui composable\n                    CounterControls(\n                        \"Counter display updated always\",\n                        counterState.counter,\n                        onDecrement, onIncrement)\n                }\n            }\n            // out of the BlocProvider composable subtree the bloc is not available\n            val bnull2= rememberProvidedBlocOf\u003cCounterBloc\u003e()\n            Log.e(LOGTAG,\"obtained bnull2 counter bloc: $bnull2\") //this must be null\n\n        }\n    }\n\n}\n\n@Composable\nfun CounterControls(\n    explanatoryText:String,\n    counterValue: Int,\n    onDecrement: () -\u003e Unit,\n    onIncrement: () -\u003e Unit\n) {\n    Text(explanatoryText)\n    Text(\"Counter value: ${counterValue}\")\n    Row {\n        Button(\n            onClick = onDecrement,\n            modifier = Modifier.padding(horizontal = 8.dp)\n        ) { Text(text = \"-\") }\n        Button(\n            onClick = onIncrement,\n            modifier = Modifier.padding(horizontal = 8.dp)\n        ) { Text(text = \"+\") }\n    }\n}\n```\n![image](https://user-images.githubusercontent.com/5619462/185851971-39ec56bb-f4ce-4f8b-9c3a-bbec98b49bc9.png)\n\n\n# Motivation\nState management in Compose as described in the [compose documentation](https://developer.android.com/jetpack/compose/state) \nis basically relying on [Android ViewModel](https://developer.android.com/jetpack/compose/state#viewmodels-source-of-truth)\nThis is very limiting in general and in particular if we want to develop for multiple platforms not \nonly for Android. \n\nA similar and connected problem is the way the [official compose docs](https://developer.android.com/jetpack/compose/navigation)\nexpect developers to handle navigation, relying on the Android specific [Jetpack Navigation Library](https://developer.android.com/guide/navigation)\n\nNavigation is such a fundamental component of an app architecture that this makes porting to\nanother platform very time consuming.\n\n[Flutter](https://flutter.dev/), also from Google,  have a very similar architecture to Android Compose and has actually\ninspired a lot the development of Compose.\nFlutter is much more mature than Compose so we thought to look at how state management is handled \nthere. \n\nFlutter documentation has a [list of the possible recommended ways](https://docs.flutter.dev/development/data-and-backend/state-mgmt/options) to handle complex state management\nThe list is quite impressive, with a lot of options that can fit the needs of any developer.\nBy the ways almost all the options listed in the official Flutter docs are actually \nopen source third party libraries. This is very typical of Flutter, which embrace open source and\ncommunity developed library much more than Android.\n\nAmong all the options there, we were particular impressed by the\n[BloC](https://felangel.github.io/bloc)  library  by Felix Angelov. This is one of the most popular Flutter libraries with almost 10k stars on github, \nvery mature (already version 8), and very well integrated with Flutter architecture (which as we said\nis very similar to Compose). So we decided to port it for Android Compose.\n\nThe result of this work is this library.\nBecause state management is usually linked to specific app screens and their lifecycle, we found the need\nto integrate state management with navigation. So we have integrated our port of [flutter_bloc](https://bloclibrary.dev/#/flutterbloccoreconcepts)\nwith a fork of the [Voyager](https://voyager.adriel.cafe/) navigation library for compose multiplatform, that is one of the most \npopular 3rd party navigation library for Compose.\n\nVoyager is generally a very well designed library\nbut is currently unmantained, with several outstanding issues and bugs, that we fixed in our fork of the library.\n\n# Differences from original voyager library\nThe original package for the code from the original library has been preserved, so switching\nto `kbloc` in the dependencies should work seamlessy:\n```groovy\ndependencies {\n    // Navigator: (multiplatform library)\n    //implementation \"cafe.adriel.voyager:voyager-navigator:$currentVersion\"\n    implementation \"io.github.beyondeye:kbloc-navigator:$currentVersion\"\n    \n    // BottomSheetNavigator  (multiplatform library)\n    //implementation \"cafe.adriel.voyager:voyager-bottom-sheet-navigator:$currentVersion\"\n    implementation \"io.github.beyondeye:kbloc-bottom-sheet-navigator:$currentVersion\"\n    \n    // TabNavigator  (multiplatform library)\n    //implementation \"cafe.adriel.voyager:voyager-tab-navigator:$currentVersion\"\n    implementation \"io.github.beyondeye:kbloc-tab-navigator:$currentVersion\"\n    \n    // Transitions  (multiplatform library)\n    //implementation \"cafe.adriel.voyager:voyager-transitions:$currentVersion\"\n    implementation \"io.github.beyondeye:kbloc-transitions:$currentVersion\"\n    \n    // Android ViewModel integration (android library)\n    //implementation \"cafe.adriel.voyager:voyager-androidx:$currentVersion\"\n    implementation \"io.github.beyondeye:kbloc-androidx:$currentVersion\"\n    \n    // Koin integration (multiplatform library)\n    //implementation \"cafe.adriel.voyager:voyager-koin:$currentVersion\"\n    implementation \"io.github.beyondeye:kbloc-koin:$currentVersion\"\n    \n    // Kodein integration ( (multiplatform library)\n    //implementation \"cafe.adriel.voyager:voyager-kodein:$currentVersion\"\n    implementation \"io.github.beyondeye:kbloc-kodein:$currentVersion\"\n    \n    // Hilt integration (android library)\n    //implementation \"cafe.adriel.voyager:voyager-hilt:$currentVersion\"\n    implementation \"io.github.beyondeye:kbloc-hilt:$currentVersion\"\n    \n    // RxJava integration (JVM library)\n    //implementation \"cafe.adriel.voyager:voyager-rxjava:$currentVersion\"\n    implementation \"io.github.beyondeye:kbloc-rxjava:$currentVersion\"\n    \n    // LiveData integration (android library)\n    //implementation \"cafe.adriel.voyager:voyager-livedata:$currentVersion\"\n    implementation \"io.github.beyondeye:kbloc-livedata:$currentVersion\"\n}\n```\n- original voyager does not support Compose Web\n- original voyager does not support path-based navigation.\n- ``voyager-androidx`` now work differently: all lifecycle handling hooks from the original code have been removed\n  (``AndroidScreenLifecycleOwner`` is not used anymore). This is because it caused many issues:\n  see for example [issue 62](https://github.com/adrielcafe/voyager/issues/62).\n  But this does not mean that android lifecycle events are ignored. When an activity is destroyed \n  then all associated `ScreenModels` and `Blocs` are automatically disposed and the associated flows cancelled\n- Also flows associated to blocs of an ``Activity`` are automatically paused when the ``Activity`` is paused\n- Now Screen lifecycle is handled in the following way: A `Screen` (and associated screen model and blocs)\n  is disposed in the following cases\n  - When the `Screen` is popped from the navigator stack\n  - When the parent `Activity` where the `Screen` composable was started is destroyed\n\n  In order to all this to work, now is required to declare the top level navigator in an\n  activity with `RootNavigator`:\n```kotlin\nclass MainScreen: Screen {\n    @Composable\n        override fun Content() {\n            //... main screen implementation here\n        }\n}\n\nclass MainActivity : ComponentActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContent {\n            //*IMPORTANT* need to use RootNavigator to initialize root navigator for activity, not as in original voyager\n            RootNavigator(MainScreen())\n        }\n    }\n}\n```\n- If Activity and Screen lifecycle as it is currently  handled is not good enough for you please open [a new issue](https://github.com/beyondeye/compose_bloc/issues/new/choose), and we will happy to improve it.\n# Will this library merged back with voyager\nCurrently the original voyager library does not seems to be mantained any more. I will be quite happy to\njoin forces with the original author(s), if they will decide that they are interested.\nThe original library was well designed and I learned a lot about Compose and Multiplatform Kotlin while working on this fork.\n# License\nCopyright 2022 by Dario Elyasy\nsee details [here](License.md)","funding_links":[],"categories":["Libraries"],"sub_categories":["Architecture"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbeyondeye%2Fcompose_bloc","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbeyondeye%2Fcompose_bloc","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbeyondeye%2Fcompose_bloc/lists"}