Write on the phone. Keep writing without a network. Sync back when there is one.
The third surface of Kite: it does not re-implement rendering,
word count, preview or content types — it asks the server for those, and only carries what the author typed
into a local database and back into the folder on the desktop.
Roadmap · Connecting · Kite · English · 简体中文
|
Articles live in Room on the phone, and the screens read only that database. In the underground, with the desktop off, with the router unplugged, writing works the same. There is no save button: when your hands stop, the text is already stored. |
When the server says the version you saw is gone, both versions are kept: your text stays put, the other side is filed beside it, and the editor offers two choices. Nothing is overwritten and nothing is merged behind your back. |
|
Categories, slug, status, scheduled publishing, and the fields the site's content types declare are all read from |
Preview is |
|
Client code comes from the server's |
Only existing |
Start the server on the desktop, bound to its address on the local network (by default it listens on the loopback only, where the phone cannot reach it):
kite run --addr 192.168.1.20:1717 # your desktop's own LAN addressOn the phone: put the same address in Settings (without /api/v1, the app adds it) → tap “Test connection” → sign in with the account the studio uses → tap “Sync now”.
That address is handed out by DHCP and will change; when it does, type the new one. The local network is plain HTTP, so use it only on a network you trust, and do not expose port 1717 to the internet — Kite has no TLS yet.
The project builds with Gradle 9.5, AGP 9.3, Kotlin 2.2 and KSP2, on JDK 21 or newer (Android Studio's bundled jbr is the easy choice), with sources on Java 17. There is no gradlew or gradle-wrapper.jar in the tree: open the folder in Android Studio, or use a Gradle you already have.
gradle :app:testDebugUnitTest :api:testDebugUnitTest
gradle :app:assembleDebug # app/build/outputs/apk/debug/app-debug.apkDependencies come from the Aliyun mirrors and the Gradle distribution from Tencent's, both configured in settings.gradle.kts. After the server's contract changes, regenerate the client:
./tools/generate-api.sh # needs a JDK; caches the generator jar, replaces api/src/main/java wholesaleThe phone keeps its own copy. Every article is in Room, screens read the local database and never wait for the network. The sync engine is the only thing that talks upwards.
Push, then pull. The other order would overwrite whatever you wrote offline.
| Case | What happens |
|---|---|
| Push | A new article is POSTed and takes the id the server gives back; an edit is PUT with the If-Match it was made against |
| Pull | updated_from asks only for what changed, paged by cursor; a listing carries no body, so known rows keep their local text and unseen ones are read whole before they are stored |
| Deletes | A separate pass with deleted_only=true; a row with unsent changes is kept and the watermark is held back |
| Conflict | On 409 both versions are kept for you to choose between; that row is not retried |
| Signed out / read only | 401 and 405 are told apart and neither is retried as a network fault |
app/ the app
├── ui/ Compose screens: list, editor, settings
├── data/ settings, repository, content types, discovery, uploads, server rendering
│ ├── local/ Room: ContentEntity / ContentDao / KiteDatabase
│ └── remote/ KiteGateway (the only place generated code is touched), cookie persistence
├── domain/ sync: SyncEngine orchestrates, Pusher / Puller / ConflictResolver do the work
└── work/ WorkManager background sync
api/ the client generated from the OpenAPI document — do not hand-edit
A few rules: dependencies point one way (ui → data → domain, and domain imports no Android); the database is the only truth; conversions between models live in exactly two places, KiteGateway and ContentMapping; the spec is the contract.
gradle :app:testDebugUnitTest21 JVM tests, plus 2 for the generated client: sync (push, pull, conflict, deletion, watermark), repository (no dirty flag without an edit, meta merging, scheduled dates), discovery (which networks to sweep), uploads (the multipart field name and the 415), server rendering (preview and word count). There are no instrumented tests and no screenshot comparison — the interactions on a device are checked by hand, and that is where the claim “it passes” ends.
It crashes and there is no stack trace
CrashLog writes uncaught exceptions to /sdcard/Android/data/com.kiteplus.kite.mobile/files/logs/crash.txt:
adb pull /sdcard/Android/data/com.kiteplus.kite.mobile/files/logs/crash.txtWith USB debugging, adb logcat -c && adb logcat *:E is quicker.
It cannot reach Kite on the desktop
- Run
kite run, notkite serve— serve only has the static site, so/api/v1/*is a 404 there. - Do not add
/api/v1to the address; the app appends it. - Phone and desktop must be on the same network; guest networks usually enable client isolation, which blocks this.
- On Windows, allow the app through the firewall for both private and public networks the first time it asks; when the network is “public”, a private-only rule does not apply.
- The desktop's address comes from DHCP and will change; retype it, or pin the address on the router.
Roadmap: milestones and engineering rules · Connecting: what works today, the candidates, and the trade-offs
Apache-2.0, the same as Kite.