# Twine's README documents Android and iOS, and its tree carries a desktopApp module

> Twine is a Kotlin and Compose Multiplatform RSS reader that syncs with FreshRSS, Miniflux and Dropbox, stores articles in SQLDelight and fetches feeds through Ktor. The project structure section lists Android and iOS targets only, while the tree also holds a desktopApp module that the download badges do not cover.

**msasikanth/twine** — Twine: A multiplatform RSS reader built using Kotlin and Compose

- Repository: https://github.com/msasikanth/twine
- Website: https://twine.sasikanth.dev/
- Stars: 2,415 · Forks: 139
- Language: Kotlin
- License: GPL-3.0
- Published: 2026-09-28 · Updated: 2026-09-28 · Language: en
- Canonical page: https://hysenlabs.com/projects/msasikanth-twine

## The project structure never mentions desktopApp

The structure section walks through five entries: `androidApp` for Android-specific code and the entry point, `iosApp` for the iOS Swift and Xcode project, `shared` for Compose Multiplatform UI logic and ViewModels, `core` for business logic, and `resources/icons` for shared icon resources. The `core` group is subdivided into `base`, `data`, `model` and `network`.

The top level of the repository tells a wider story. Alongside those directories sit `desktopApp/`, plus `fastlane/`, `release/`, `spotless/` and a `worktrees/` directory. So a desktop target is present in the tree and absent from the written structure, which describes a two-platform project.

The download section agrees with the structure section rather than the tree. It carries three destinations: the Google Play listing for `dev.sasikanth.rss.reader`, the Apple App Store listing, and an IzzyOnDroid package page. No desktop channel appears there, so a desktop build exists in the source layout without a documented way to obtain it.

## The local database is linked at a 2.0.0 alpha

Twine stores articles locally, and the storage layer is SQLDelight, which lives in the `data` module alongside repositories and sync logic. The tech stack entry for it points at a versioned URL:

```
https://cashapp.github.io/sqldelight/2.0.0-alpha05/
```

That is an alpha build of SQLDelight 2.0.0, referenced from a project whose own tags run v3.7.0 on 2026-08-20, v3.8.0 on 2026-08-30 and v3.9.0 on 2026-09-09. The gap is worth naming because the storage layer is the one component where an alpha dependency is hardest to swap later, since the schema and the queries that run against it sit on either side of it.

The rest of the layer split is clean. `network` uses Ktor for fetching feeds and for parsing them, `model` holds domain models shared across the project, and `base` collects utilities, common interfaces and platform abstractions. ViewModels live in `shared` and use kotlin-inject for wiring.

A full dependency list is delegated to a version catalog at `/gradle/libs.versions.toml`, which is also where the Android Gradle Plugin version is defined.

## The only command shown formats code rather than building the app

The development section says the repository can be cloned and built locally without changes, and that the project requires JDK 21 or newer. It then points at the Android Gradle Plugin version inside the version catalog to work out which Android Studio to use.

Across the whole README there is one runnable command, and it does not produce an application:

```bash
./gradlew spotlessApply
```

That is the formatting step, run before raising a pull request. The project uses ktfmt, supplied through the spotless Gradle plugin and configured with the bundled IntelliJ codestyle, and there is a `spotless/` directory at the top level alongside the plugin configuration. No assemble, build or install command is given, so the only invocation a reader can copy out of the documentation is a code formatter.

Contribution is gated differently from most projects. Bug fixes go in as pull requests; anything else is asked to open an issue first to start a conversation.

## Dropbox is the only sync backend marked Alpha

Cloud sync is listed as working with FreshRSS, also called GReader, Miniflux, and Dropbox. The first two are self-hosted services you run yourself, and neither carries a status marker. Dropbox carries one, an Alpha label with a warning symbol.

So of the three backends, two are complete and one is declared pre-release, and the distinction is not just cosmetic. A self-hosted sync backend is a server you already control and can point at an existing instance, while a file-storage backend introduces a third party and a different set of credentials to manage. Anyone who relies on sync should treat the two self-hosted paths as the supported ones and treat Dropbox as something that may change shape.

Alongside sync, the feature list covers RDF, RSS, Atom and JSON feeds, feed grouping and pinning, background sync, home screen widgets, OPML import and export, blocked words for filtering by keyword, bookmarks for later reading, search across posts, an audio player for podcasts and HTML audio tags, and an article shortcut that fetches the full article into the reader view.

## Dynamic theming takes its palette from feed content

The theming claim links to the user-generated color documentation in the design system, the mechanism that derives an interface palette from the user's wallpaper. Twine uses the same idea with a different input: the description says the ambient app color changes based on content rather than the system wallpaper.

That distinction is the whole mechanism. On Android the system dynamic color pipeline is driven by the wallpaper and the system theme, so an app built on it inherits whatever palette the device already has. Twine reads the feed item and applies a color from it, which means a feed full of one kind of imagery produces a consistent tint, while a mixed feed produces an ambient color that shifts as you move between items.

The reader view is separately customizable for fonts and colors, and there are light, dark and amoled theme options alongside the dynamic mode. Crowdin handles translations, and strings come from Compose resources rather than from a platform resource file per target, which keeps one string table across both platforms instead of two.

## Three tool configs and a docs site sit at the repository root

The top level is busier than the structure section suggests. `.idea/` is committed, as is `.junie/` and `.geminiignore`, and `AGENTS.md` sits alongside them. That is an IDE configuration directory, two separate AI coding tool configurations, and an agent instruction file, all versioned in the same repository as the application.

There is also a documentation site published from source: `CNAME`, `index.html` and `.nojekyll` together are the shape of a GitHub Pages site served from the repository root, with `readme_images/` holding the assets for it. A `worktrees/` directory is committed as well, which normally holds scratch checkouts rather than shared content.

Dependency updates are automated through `renovate.json5`, and releases go through the `fastlane/` directory. The practical consequence for a reader is that the documented five-entry structure accounts for a minority of what is actually at the root.

## Two non-Gradle scripts handle translations and cleanup

A Kotlin Multiplatform project usually keeps its tooling inside the Gradle build. Twine keeps two scripts beside the Gradle wrapper instead: `check_translations.py`, written in Python, and `cleanup.sh`.

The translation one follows from the Crowdin setup. Strings are managed on Crowdin, the repository carries a `crowdin.yml` at the root, and the project reads its strings through Compose resources rather than through separate Android and iOS resource files. Verifying that a translated string exists in every locale is a natural thing to script, and the choice to write that script in Python rather than as a Gradle task means it runs outside the build, with its own interpreter requirement.

The cleanup script has no stated purpose in the documentation. Between them, the two scripts add a Python dependency and a shell environment to a project whose documented build requirement is otherwise a single JDK version.

## Conclusion

Twine is worth a look if you want a Compose Multiplatform reader with real background sync and self-hosted options, since FreshRSS and Miniflux backends put it ahead of a plain offline reader, and the blocked-words filter plus OPML round tripping make it usable as a daily reader rather than a demo. Verify two things before depending on it. The desktopApp module exists in the tree but appears in neither the project structure list nor the download section, so treat desktop support as undocumented rather than absent. And Dropbox sync is marked Alpha while the two self-hosted backends are not, so plan on FreshRSS or Miniflux for anything you care about. The build side is honest but thin: JDK 21 is the stated requirement and the only command shown formats code. GPL-3.0 covers the repository.

## FAQ

### Which platforms does the Twine RSS reader ship for?

The download section offers three destinations: a Google Play listing, an Apple App Store listing, and an IzzyOnDroid package page. The written project structure names `androidApp` and `iosApp`, while a `desktopApp/` directory exists in the tree without appearing in either list.

### How does Twine store articles locally and what fetches feeds?

Local storage is SQLDelight, held in the `data` module together with repositories and sync logic, with domain models in `model`. The `network` module uses Ktor both for fetching feeds and for parsing them.

### Which cloud sync services does Twine support?

Three are named: FreshRSS, also called GReader, Miniflux, and Dropbox. The Dropbox entry carries an Alpha marker, while the two self-hosted backends do not.

### What does Twine require to build the project locally?

JDK 21 or newer, with the Android Gradle Plugin version defined in `gradle/libs.versions.toml` used to pick a matching Android Studio. The only command shown in the README is `./gradlew spotlessApply`, which formats code rather than assembling an app.

### What feed formats and import formats does Twine read?

RDF, RSS, Atom and JSON feeds, with OPML used for importing and exporting feed lists. Given any website homepage, Twine looks for the feed on its own rather than requiring the feed URL.

## Sources

- [License: GPL-3.0](https://github.com/msasikanth/twine/blob/main/LICENSE)
- [msasikanth/twine on GitHub](https://github.com/msasikanth/twine)
- [Project website](https://twine.sasikanth.dev/)
- [README](https://github.com/msasikanth/twine/blob/main/README.md)
- [Releases](https://github.com/msasikanth/twine/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/msasikanth-twine
