A small Compose Multiplatform app (Android + Desktop/JVM + iOS) that demonstrates the
ktor-client-kiche HTTP/3 client against public test servers. UX follows the Compose Multiplatform
sample: a menu of buttons, each opening a feature screen.
Modules (the KMP plugin can't coexist with com.android.application in AGP 9, hence the split):
:example:shared— KMP library with the shared Compose UI/ViewModel, the desktop app entry, and the iOSComposeAppframework +MainViewController.:example:androidApp— pure Android application that depends on:example:shared.example/iosApp— iOS app (not a Gradle module): an Xcode project generated by XcodeGen fromproject.yml, embedding theComposeAppframework. SeeiosApp/README.md.
| Screen | What it does | Endpoint |
|---|---|---|
| Connection & protocol | QUIC + TLS 1.3 handshake; shows the negotiated protocol (expect HTTP/3.0) | nghttp2.org/httpbin/get |
| Echo (POST body) | POSTs a body, reads it back from the response JSON | nghttp2.org/httpbin/post |
| Download bytes | GETs a fixed number of bytes and times it | nghttp2.org/httpbin/bytes/{n} |
| Streamed response | Reads a chunked, newline-delimited streaming response | nghttp2.org/httpbin/stream/{n} |
A single pooled HttpClient is shared across screens (Http3DemoViewModel), so you can see
Kiche's connection pooling: the first request handshakes, later ones reuse the connection.
Desktop (works out of the box on macOS — :kiche bundles a prebuilt libquiche_jni.dylib):
./gradlew :example:shared:run
Android requires the quiche JNI .so built for the device ABI via cargo-ndk (through :kiche).
This path is not yet integration-tested. Once built:
./gradlew :example:androidApp:installDebug
iOS needs Xcode + XcodeGen (brew install xcodegen). The .xcodeproj is generated, not committed:
cd example/iosApp && xcodegen generate && open iosApp.xcodeproj
Pick a simulator and Run. The shared framework is built/embedded by an Xcode pre-build script; the
first build also compiles quiche for iOS (slow once). Details in iosApp/README.md.
The app sets verifyPeer = false in the Kiche engine on purpose, to avoid bundling a CA
file. Kiche has no system trust store yet, so real verification needs an explicit caCertPath.
This is fine for a demo against known servers — never ship verifyPeer = false in a real app. To
verify properly, bundle a CA bundle (e.g. https://curl.se/ca/cacert.pem) and set caCertPath.
readUTF8Linedeprecation (Streamed response screen).ByteReadChannel.readUTF8Line()is deprecated in ktor 3.4 (usereadLine/readLineStrict). We keepbodyAsChannel()+readUTF8Line()because demonstrating the channel-based streaming-read pattern is the point of that screen. Tolerated for now; migrate when convenient.- Streaming is not yet truly incremental. The engine buffers the full response body before exposing it, so the streamed lines are counted from the assembled body. Real incremental delivery awaits engine-level response-body streaming.
- iOS runs the same shared Compose UI via
ComposeUIViewController. The whole Kotlin/native chain (Compose UI →ktor-client-kiche→:kiche→libquiche.a) links into theComposeAppframework; build and run from Xcode — seeiosApp/README.md.