{"id":21517442,"url":"https://github.com/criske/compose-ssr","last_synced_at":"2026-05-17T21:03:01.245Z","repository":{"id":126257704,"uuid":"435138097","full_name":"criske/compose-ssr","owner":"criske","description":"Proof of concept of using server side rendering with jetpack compose","archived":false,"fork":false,"pushed_at":"2021-12-05T11:59:35.000Z","size":4876,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-01-31T03:31:48.997Z","etag":null,"topics":["android","jetpack-compose","kotlin","server-driven-ui","server-side-rendering"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/criske.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"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":"2021-12-05T10:43:18.000Z","updated_at":"2024-07-16T13:45:08.000Z","dependencies_parsed_at":null,"dependency_job_id":"d6a1c5c1-21f4-4a9b-8a46-dd0e6d1c1dc0","html_url":"https://github.com/criske/compose-ssr","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/criske%2Fcompose-ssr","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/criske%2Fcompose-ssr/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/criske%2Fcompose-ssr/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/criske%2Fcompose-ssr/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/criske","download_url":"https://codeload.github.com/criske/compose-ssr/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244068163,"owners_count":20392758,"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","jetpack-compose","kotlin","server-driven-ui","server-side-rendering"],"created_at":"2024-11-24T00:41:46.487Z","updated_at":"2026-05-17T21:03:01.196Z","avatar_url":"https://github.com/criske.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# compose-ssr\nProof of concept of using server side rendering with jetpack compose\n\n\nKey components:\n- SSRService: gateway to the remote server renderer. For testing purposes this POF comes with a fake SSRService that can be used locally instead (`FakeSSRService`).\n- ComponentComposer: takes the tree of Components resulted from deserializing the remote json obtained via SSRService and map them to composable ui functions. For deserialization see ComponentDeserializer.\n- Android specifics:\n    - SSRActivity: bootstrap the ComponentComposer's composables.\n    - SSRViewModel: provides deserialized Components to ComponentComposer as StateFlow. It also provides backstack support.\n\n- Interceptors: These are the main controllers of the application and must be implemented by client. They sit between SSRViewModel and SSRService and can intercept requests and responses. They also are responsible for interaction between the SSRActivity and SSRViewModel, via a Interactor interface. They have the following the interception point:\n    - `interceptFromClient` intercepts requests from client to server. must be paired with `acceptFromClient(UriMatching)`\n    - `interceptFromServer` intercepts responses from server to client. must be paired with `acceptFromServer(UriMatching)`\n    - `onInteract` interaction with SSRViewModel. The provided `ComponentContext` selects components by their id. For example here is where you register click listeners. must be paired with `acceptInteractScreen(screenId)`\n    - `onCompose` takes control for generating ui composables functions for a screen instead of letting the `ComponentComposer` to do its job. Also provided with `ComponentContext`. must be paired with `acceptComposeScreen(screenId)`. Here is an example:\n    ```kotlin\n    //dsl\n    interceptor {\n        onCompose(\"loginScreen\")  {\n            @Composable {\n                Box(modifier = Modifier.fillMaxSize()) {\n                    id\u003cComponent.Text\u003e(\"title\") {\n                        Text(text = text, modifier = Modifier.align(Alignment.Center))\n                    }\n                }\n            }\n        }    \n    }\n    ```\n    For convenience the framework provides a DSL to create interceptor.\n\n- SSRInstaller: setup builder to add interceptors, specify the SSRService implementation and the initial uri entry point for the remote server. The SSRInstaller must be installed in an `Application#onCreate()`\n\n\nNote: for now the the frawework follows a specific json schema and using Gson.  In the future  clients will be their own deserializers using their favorite json parser.\n\n\n\n\n## Example: clicker\n\nAfter 3 clicks app ends.\nUses a FakeSSRService with 3 seconds response latency.\n\ninstalling\n```kotlin\nclass MyApp : Application() {\n\n    override fun onCreate() {\n        super.onCreate()\n        SSRInstaller(\"https://clicker.net\")\n            .service(\n                FakeSSRService(\n                    handlers = listOf(ClickerSSRHandler()),\n                    latencyMillisProvider = {3000}\n                )\n            )\n            .interceptors(\n                splashScreenInterceptor,\n                clickerInterceptor(Gson())\n            )\n            .install(this)\n    }\n}\n```\n\nfake ssr handler (in a real scenario this is remote server's job):\n```kotlin\nclass ClickerSSRHandler : SSRHandler {\n\n    private val matcher = matching(\"clicker.net\", \"/\", \"/count/#\")\n\n    companion object {\n        private val TEMPLATE: (Int) -\u003e String = { count -\u003e\n            \"\"\"\n                {\n                    \"id\" : \"count-screen\",\n                    \"type\" : \"screen\",\n                    \"content\": {\n                        \"id\" : \"content\",\n                        \"type\": \"container\",\n                        \"alignment\" : {\n                            \"main-axis\" : \"center\",\n                            \"cross-axis\" : \"center\"\n                        },\n                        \"children\" : [\n                         {\n                            \"id\": \"count-txt\",\n                            \"type\": \"text\",\n                            \"text\": \"$count\",\n                            \"style\": {\n                                \"font-weight\" : 600,\n                                \"font-size\": 64\n                            }\n                        },\n              \n                            {\n                                \"id\" : \"count-btn\",\n                                \"type\" : \"button\",\n                                \"text\" : \"Click me!\"\n                            }\n                        ]\n                    }\n                }\n            \"\"\".trimIndent()\n        }\n    }\n\n    override fun accept(uri: Uri): Boolean = matcher.matches(uri)\n\n    override fun handle(uri: Uri, jsonBody: String): Rendered {\n        val query = uri.pathSegments.run {\n            val countIndex = indexOf(\"count\")\n            if (countIndex != -1 \u0026\u0026 countIndex \u003c lastIndex) {\n                this[countIndex + 1].toInt() + 1\n            } else {\n                0\n            }\n        }\n        return Rendered(Response(uri, TEMPLATE(query)))\n    }\n}\n```\n\nclick interceptor.\n\n```kotlin\nval clickerInterceptor: (Gson) -\u003e Interceptor = { gson -\u003e\n\n    interceptor {\n\n        val matchClickRequest = matching(\"clicker.net\", \"/count/#\")\n\n        fromClient(matchClickRequest) {\n            sendBackToClientOnSlowRequest {\n                val disabledClickBtn = gson.edit(this.request.currentScreen) { json -\u003e\n                    val clickBtn = json.obj(\"content/children[1]\")\n                    clickBtn[\"disabled\"] = true\n                }\n                Response(this.request.uri, disabledClickBtn)\n            }\n        }\n\n        onInteract(\"count-screen\") { interactor, _ -\u003e\n            val count = id\u003cComponent.Text\u003e(\"count-txt\")?.text?.toInt() ?: 0\n            if (count == 3) {\n                interactor.debugToast(\"Hooray you've count to 3. Good bye for now!\")\n                interactor.closeApp()\n            }\n            id\u003cComponent.Button\u003e(\"count-btn\") {\n                onClick = {\n                    interactor.request(\"https://clicker.net/count/$count\")\n                }\n            }\n        }\n\n    }\n}\n```\n\n\u003cimg src=\"assets/clicker-sample.gif\" width=\"25%\"/\u003e\n\n\n## Example: Two screens app (login + dashboard).\n\nThe login interceptor checks if inputs are not empty sparing for an extra trip to server.\nLogin json screen offers theming too. (right now only colors are supported).\n\nThe dashboard interceptor offers pagination.\n\n\n\u003cimg src=\"assets/login-sample.gif\" width=\"25%\"/\u003e\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcriske%2Fcompose-ssr","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcriske%2Fcompose-ssr","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcriske%2Fcompose-ssr/lists"}