{"id":19627166,"url":"https://github.com/tunjid/me","last_synced_at":"2025-04-04T17:05:33.666Z","repository":{"id":45796746,"uuid":"439670817","full_name":"tunjid/me","owner":"tunjid","description":"A Jetpack Compose Kotlin Multiplatform WYSIWYG blog editor","archived":false,"fork":false,"pushed_at":"2024-11-24T22:37:29.000Z","size":5878,"stargazers_count":206,"open_issues_count":1,"forks_count":13,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-28T16:06:21.900Z","etag":null,"topics":["android","compose","jetpack-compose","kotlin","kotlin-multiplatform-sample","multiplatform"],"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/tunjid.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":"contributing.md","funding":null,"license":"LICENSE.txt","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-18T17:06:46.000Z","updated_at":"2025-03-10T08:41:20.000Z","dependencies_parsed_at":"2023-12-21T01:38:58.105Z","dependency_job_id":"167da7c7-d29a-4b4a-972b-55aa758ce318","html_url":"https://github.com/tunjid/me","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/tunjid%2Fme","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tunjid%2Fme/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tunjid%2Fme/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tunjid%2Fme/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tunjid","download_url":"https://codeload.github.com/tunjid/me/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247217174,"owners_count":20903008,"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","compose","jetpack-compose","kotlin","kotlin-multiplatform-sample","multiplatform"],"created_at":"2024-11-11T11:48:38.994Z","updated_at":"2025-04-04T17:05:33.640Z","avatar_url":"https://github.com/tunjid.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Me\n\nPlease note, this is not an official Google repository. It is a Kotlin multiplatform experiment\nthat makes no guarantees about API stability or long term support. None of the works presented here\nare production tested, and should not be taken as anything more than its face value.\n\n## Introduction\n\n\"Me\" is a Kotlin Multiplatform playground for ideas that pop into my head around app architecture.\nThese ideas typically center around state, and it's production; a repository of \"what ifs?\".\n\nIt follows the modern android\ndevelopment [architecture guide](https://developer.android.com/topic/architecture), and\nattempts to extend it to envision what building apps will look like in the near future,\nwith novel/experimental implementations of fundamental app architecture units including:\n\n* Navigation\n* Pagination\n* UI State production\n* Dependency injection\n* Persistent animation\n* Large screen experiences\n\nAll while targeting multiple platforms and meeting stringent product requirement constraints like:\n\n* No pull to refresh in the app. The app is always up to date.\n* Navigation is persisted between app restarts and device reboots.\n* Scroll position is preserved between app restarts and device reboots.\n\nThe app is a WYSIWYG editor for my personal website. The source for the backend can be\nfound [here](https://github.com/tunjid/tunji-web-deux).\n\n![Demo image](https://github.com/tunjid/me/blob/main/misc/demo.gif)\n\nSome ideas explored include:\n\n* [Mutators](https://github.com/tunjid/Mutator) as abstract data types for the production and\n  mutationOf of state\n* Reactive app architecture as a driver of app state\n* Android insets and IME (keyboard) behavior as state\n* Android permissions as state\n* [Tiling](https://github.com/tunjid/Tiler) for incremental loading (pagination) as state\n* [Trees](https://github.com/tunjid/treeNav) for representing app navigation as state\n* [Jetpack Compose](https://developer.android.com/jetpack/compose?gclid=CjwKCAiA4KaRBhBdEiwAZi1zzpXxpbbQ5-qXVpv8RHzJKKCDY_Yv7AXMLpeRHaMCK-SNVI9i4jvJ4RoC_VQQAvD_BwE\u0026gclsrc=aw.ds)\n  for\n  stateful [motionally intelligent](https://medium.com/androiddevelopers/motional-intelligence-build-smarter-animations-821af4d5f8c0)\n  global UI\n\n🚨⚠️🚧👷🏿‍♂️🏗️🛠️🚨\n\nI try to keep the code at a near production quality, but this often takes a back seat to\nconvenience and whim.\n\nAgain, the work presented here are the experiments of an immutable state and functional reactive\nprogramming zealot.\nIt's far from objective, caveat emptor.\n\n## Architecture\n\n### Data layer\n\n#### Offline-first\n\nThe app is a subscriber in a pub-sub liaison with the server. There is no pull to refresh, instead\nthe app pulls diffs\nof `ChangeListItem` when the server notifies the app of changes made.\n\nThe following rules are applied to the data layer:\n\n* DAOs are internal to the data layer\n* DAOs expose their data with reactive types (`Flow`)\n* Reads from the data layer NEVER error.\n* Writes to the data layer may error and the error is bubbled back up to the caller\n* The `NetworkService` is internal to the data layer\n\n#### Pub sub implementation\n\nPub sub in the app is backed by a change list invalidation based system. Its premise is:\n\n* Each model table on the server will have a sibling table that has a row that tracks a unique id\n  that identifies a CRUD\n  update (change_list_id). This unique id must have natural ordering.\n* CRUD updates to any model will cause an update for the change_list_id (akin to a new commit in\n  git).\n* The client will then hit an endpoint asking for changes since the last change_list_id it has, or\n  its local HEAD. A\n  changelist of model ids that have changed will then be sent (akin to a git fetch)\n* The clients will then chew on the change list incrementally, updating its local HEAD as each\n  update is consumed (akin\n  to applying the pulled commits).\n\nReal time updates are implemented with websockets via [socket.io](https://socket.io/). I intend to\nmove the android\nclient to FCM for efficiency reasons in the future.\n\n#### Models\n\nThe app offers 3 main data types:\n\n* `Archive`: Content I've produced over the years: Articles, projects and talks.\n* `User`: The creator of the content shown. This is really just me.\n* `SavedState`: App saved state. This is navigation state, and screen sate of each navigation\n  destination.\n\n### Domain Layer\n\nThe domain layer offers abstractions that consolidate common patterns and business logic across each\nfuture and its screen. There are two main types here:\n\n* `NavStateHolder`: Manages the app navigation state and interacts with the `SavedStateRepository`.\n  It provides Navigation as state.\n* `GlobalUiStateHolder`: Manages configuration for the app and adapts it over different form factors\n  and screen sizes. It provides app level UI as state.\n\n### UI Layer\n\n#### State production\n\nAll screen level state holders are implemented\nwith [unidirectional data flow as a functional declaration](https://www.tunjid.com/articles/unidirectional-data-flow-as-a-functional-declaration-6230b74f5d785a7ebc8c2a43).\n\n### X as state\n\n#### Navigation as state\n\nThis app treats navigation as state, and as such, it is completely managed by business logic. The\nNavigation state\nis persisted in the data layer with the `SavedStateRepository` and exposed to the app via\nthe `NavStateHolder`.\n\nEach destination in the app is represented by an `AppRoute` that exposes a single `@Composable`\n`Render()` function. The backing data structures for navigation are the tree\nlike [`StackNav`](https://github.com/tunjid/treeNav/blob/develop/treenav/src/commonMain/kotlin/com/tunjid/treenav/StackNav.kt)\nand\n[`MultiStackNav`](https://github.com/tunjid/treeNav/blob/develop/treenav/src/commonMain/kotlin/com/tunjid/treenav/MultiStackNav.kt)\nimmutable classes. The root of the app is a `MultiStackNav` and navigation is\ncontrolled by a `NavStateHolder` defined as:\n\n```\ntypealias NavStateHolder = ActionStateMutator\u003cMutation\u003cMultiStackNav\u003e, StateFlow\u003cMultiStackNav\u003e\u003e\n```\n\n#### Global UI as state\n\nThe app utilizes a single bottom nav, toolbar and a shared global UI state as defined by the\n`UiState` class. This is what allows for the app to have responsive navigation while accounting\nfor visual semantic differences between Android and desktop. Android for example uses the\n`WindowManager` API to drive it's responsiveness whereas desktop just watches it's `Window` size.\nThe definition for the `GlobalUiStateHolder` is:\n\n```\ntypealias GlobalUiStateHolder = ActionStateMutator\u003cMutation\u003cUiState\u003e, StateFlow\u003cUiState\u003e\u003e\n```\n\n#### Paging as state\n\nPagination is implemented as a function of the current page and number of columns in the grid:\n\n```\n[out of bounds]                    -\u003e Evict from memory\n                                                   _\n[currentPage - gridSize - gridSize]                 |\n...                                                 | -\u003e Keep pages in memory, but don't observe\n[currentPage - gridSize - 1]   _                   _|                        \n[currentPage - gridSize]        |\n...                             |\n[currentPage - 1]               |\n[currentPage]                   |  -\u003e Observe pages     \n[currentPage + 1]               |\n...                             |\n[currentPage + gridSize]       _|                  _\n[currentPage + gridSize + 1]                        |\n...                                                 | -\u003e Keep pages in memory, but don't observe\n[currentPage + gridSize + 1 + gridSize]            _|\n\n[out of bounds]                    -\u003e Evict from memory\n```\n\nAs the user scrolls, `currentPage` changes and new pages are observed to keep the UI relevant.\n\n#### State restoration and process death\n\nAll types that need to be restored after process death implement the `ByteSerializable` interface.\nThis allows them to de serialized compactly into a `ByteArray` which can then be saved to disk with\na\n[`DataStore`](https://developer.android.com/topic/libraries/architecture/datastore?gclid=CjwKCAjwtp2bBhAGEiwAOZZTuOs3XNmaNxY65HGo2wnRPqvKt1c18A1dhe4sETq_A3Iyx8DDv6uA1xoCv9kQAvD_BwE\u0026gclsrc=aw.ds)\ninstance. The bytes are read or written with a type called the `ByteSerializer`.\n\nThings restored after process death currently include:\n\n* App navigation\n* The state of each `AppRoute` at the time of process death\n\n#### Lifecycles and component scoping\n\nScreen state holders are scoped to the navigation state. When a route is removed from the navigation\nstate, it's\nstate holder has it's `CoroutineScope` cancelled:\n\n```kotlin\nappScope.launch {\n            navStateStream\n                .map { it.mainNav }\n                .removedRoutes()\n                .collect { removedRoutes -\u003e\n                    removedRoutes.forEach { route -\u003e\n                        println(\"Cleared ${route::class.simpleName}\")\n                        val holder = routeStateHolderCache.remove(route)\n                        holder?.scope?.cancel()\n                    }\n                }\n        }\n```\n\nLifecycles aware state collection is done with a custom `collectAsStateWithLifecycle` backed by the\nfollowing lifecycle\ndefinition:\n\n```kotlin\ndata class Lifecycle(\n    val isInForeground: Boolean = true,\n)\n```\n\n## Running\n\nAs this is a multiplatform app, syntax highlighting may be broken in Android studio. You may fare\nbetter building with Intellij.\n\nDesktop: `./gradlew :desktop:run`\nAndroid: `./gradlew :android:assembleDebug` or run the Android target in Android Studio\n\n## License\n\n    Copyright 2021 Google LLC\n\n    Licensed under the Apache License, Version 2.0 (the \"License\");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n\n        https://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distributed on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftunjid%2Fme","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftunjid%2Fme","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftunjid%2Fme/lists"}