{"id":26106026,"url":"https://github.com/tunjid/heron","last_synced_at":"2026-03-10T14:06:51.000Z","repository":{"id":268764230,"uuid":"895341915","full_name":"tunjid/heron","owner":"tunjid","description":"A graceful multiplatform bluesky client","archived":false,"fork":false,"pushed_at":"2026-03-06T17:31:08.000Z","size":71064,"stargazers_count":472,"open_issues_count":24,"forks_count":31,"subscribers_count":2,"default_branch":"main","last_synced_at":"2026-03-06T19:30:58.606Z","etag":null,"topics":["architecture","architecture-components","bluesky-client","compose","compose-desktop","compose-ios","coroutines","jetpack-datastore","jetpack-lifecycle","jetpack-room","kotlin","kotlin-multiplatform-sample","kotlinx-coroutines","kotlinx-datetime","kotlinx-serialization","pagination"],"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":null,"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,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-11-28T02:59:59.000Z","updated_at":"2026-03-06T17:31:13.000Z","dependencies_parsed_at":null,"dependency_job_id":"7ee445c6-d8e1-4afb-8620-dd115e7f114f","html_url":"https://github.com/tunjid/heron","commit_stats":null,"previous_names":["tunjid/heron"],"tags_count":50,"template":false,"template_full_name":null,"purl":"pkg:github/tunjid/heron","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tunjid%2Fheron","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tunjid%2Fheron/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tunjid%2Fheron/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tunjid%2Fheron/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tunjid","download_url":"https://codeload.github.com/tunjid/heron/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tunjid%2Fheron/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30336122,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-10T12:41:07.687Z","status":"ssl_error","status_checked_at":"2026-03-10T12:41:06.728Z","response_time":106,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"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":["architecture","architecture-components","bluesky-client","compose","compose-desktop","compose-ios","coroutines","jetpack-datastore","jetpack-lifecycle","jetpack-room","kotlin","kotlin-multiplatform-sample","kotlinx-coroutines","kotlinx-datetime","kotlinx-serialization","pagination"],"created_at":"2025-03-09T21:54:52.428Z","updated_at":"2026-03-10T14:06:50.992Z","avatar_url":"https://github.com/tunjid.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Heron\n\nHeron is a Jetpack Compose adaptive, reactive and offline-first Bluesky client.\n\n## Download\n\u003ca href=\"https://play.google.com/store/apps/details?id=com.tunjid.heron\u0026referrer=utm_campaign%3Dandroid_metadata%26utm_medium%3Dweb%26utm_source%3Dgithub.com%26utm_content%3Dbadge\" target=\"_blank\"\u003e\u003cimg src=\"https://upload.wikimedia.org/wikipedia/commons/7/78/Google_Play_Store_badge_EN.svg\" alt=\"Get it on Google Play\" height=\"48\"\u003e\u003c/a\u003e\n\n## Screenshots\n\n| ![Scroll animations](./docs/images/1.gif) | ![Screen transitions](./docs/images/2.gif) | ![Thread diving](./docs/images/3.gif) |\n|-------------------------------------------|--------------------------------------------|---------------------------------------|\n\n## UI/UX and App Design\n\nHeron uses [Material design and motion](https://m3.material.io/) and is heavily inspired by the\n[Crane](https://m2.material.io/design/material-studies/crane.html) material study.\n\nLibraries:\n\n1. Geometric shapes are created with\n   the [Jetpack shapes](https://developer.android.com/jetpack/androidx/releases/graphics) graphics\n   library.\n2. UX patterns like pinch to zoom, drag to dismiss, collapsing headers, multipane layouts, and\n   and so on are implemented with the [Composables](https://github.com/tunjid/composables) library.\n\n## Architecture\n\nThis is a\nmulti-module [Kotlin Multiplatform](https://www.jetbrains.com/help/kotlin-multiplatform-dev/get-started.html)\nproject targeting Android, iOS and Desktop that follows\nthe [Android architecture guide](https://developer.android.com/topic/architecture).\n\nFor more details about this kind of architecture, take a look at\nthe [Now in Android sample](https://github.com/android/nowinandroid) repository,\nthis app follows the same architecture principles it does, and the architecture decisions are very\nsimilar.\n\nThere are 6 kinds of modules:\n\n1. `data-*` is the [data layer](https://developer.android.com/topic/architecture/data-layer) of the\n   app containing models data and repository implementations for reading and writing that data.\n   Data reads should never error, while writes are queued with a `WriteQueue`.\n    - [Jetpack Room](https://developer.android.com/jetpack/androidx/releases/room)\n      is used for persisting data with SQLite.\n        - Given the highly relational nature of the app, a class called a `MultipleEntitySaver` is\n          used to save bluesky\n          network models.\n    - [Jetpack DataStore](https://developer.android.com/jetpack/androidx/releases/datastore)\n      is used for blob storage of arbitrary data with protobufs.\n    - [Ktor](https://ktor.io/) is used for network connections via the\n      [Ozone at-proto bindings](https://github.com/christiandeange/ozone).\n2. `domain-*` is the [domain layer](https://developer.android.com/topic/architecture/domain-layer)\n   where aggregated business logic lives. This is mostly `domain-timeline` where higher level\n   abstractions timeline data manipulation exists.\n3. `feature-*` contains navigation destinations or screens in the app. Multiple features can\n   run side by side in app panes depending on the navigation configuration and device screen size.\n4. `ui-*` contains standalone and reusable UI components and Jetpack Compose effects for the app\n   ranging from basic layout to multimedia components for displaying photos and video playback.\n5. `scaffold` contains the application state in the `AppState` class, and coordinates app level\n   UI logic like pane display, drag to dismiss, back previews and so on. It is the entry point to\n   the multiplatform application\n6. `/composeApp` is app module that is the fully assembled app and depends on all other modules.\n   It offers the entry point to the application for a platform.\n   It contains several subfolders:\n    - `commonMain` is for code that’s common for all targets.\n    - Other folders are for Kotlin code that will be compiled for only the platform indicated in the\n      folder name.\n    - `/iosApp` is the entry point for the iOS app.\n\n### Dependency Injection\n\nDependency injection is implemented with the [Metro](https://github.com/ZacSweers/metro)\nlibrary which constructs the dependency graph at build time\nand therefore is compile time safe. Assisted injection is used for feature screens to pass\nnavigation arguments information to the feature. The items in the dependency graph are:\n\n* `NavigationBindings` from feature modules providing Navigation3 `NavEntry` instances per feature.\n* Feature `Bindings` from feature modules providing access to the data layer and app scaffold to each module.\n* Scaffold `Bindings` providing the `PaneScaffoldState` for building a multi-pane app,\n* Data `Bindings` for the data layer.\n* An `AppNavigationGraph` for resolving navigation routes.\n* An `AppGraph` containing the entire app DI graph.\n\n### Navigation\n\nNavigation uses the [treenav experiment](https://github.com/tunjid/treeNav) to implement\nAndroid [adaptive navigation](https://developer.android.com/develop/ui/compose/layouts/adaptive).\nSpecifically it uses a `ThreePane` configuration, where up to 3 navigation panes may be shown, with\none reserved for back previews and another for modals. Navigation state is also saved to disk and\npersisted across app restarts.\n\n### State production\n\n* State production follows\n  the [Android guide to UI State Production](https://developer.android.com/topic/architecture/ui-layer/state-production).\n* Each feature uses a\n  single [Jetpack ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel)\n  as\n  the [business logic state holder](https://developer.android.com/topic/architecture/ui-layer/stateholders).\n* State is produced in a lifecycle aware way using\n  the [Jetpack Lifecyle](https://developer.android.com/jetpack/androidx/releases/lifecycle) APIs.\n    * The `CoroutineScope` for each `ViewModel` is obtained from the composition's\n      `LocalLifecycleOwner`\n* The specifics of producing state over time is implemented with\n  the [Mutator library](https://github.com/tunjid/Mutator).\n    * Inputs to the state production pipeline are passed to the mutator in the `inputs` argument, or\n      derived from an action in `actionTransform`.\n    * Every coroutine launched is limited to running when the lifecycle of the component displaying\n      it is resumed. When the lifecyle\n      is paused, the coroutines are cancelled after 2 seconds:\n      `SharingStarted.WhileSubscribed(FeatureWhileSubscribed)`.\n    * Each user `Action` is in a sealed hierarchy, and action parallelism is defined by the\n      `Action.key`. Actions with different keys run in parallel\n      while those in the same key are processed sequentially. Each distinct subtype of an `Action`\n      hierarchy typically has it's own\n      key unless sequential processing is required for example:\n        * All subtypes of `Action.Navigation` typically share the same key.\n        * All subtypes of pagination actions, also share the same key and are processed with\n          the [Tiling library](https://github.com/tunjid/Tiler).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftunjid%2Fheron","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftunjid%2Fheron","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftunjid%2Fheron/lists"}