A lightweight dynamic UI engine for Android that renders native Jetpack Compose widgets from JavaScript scripts at runtime, powered by QuickJS.
No WebView. No HTML. Pure native UI driven by scripts.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β JavaScript (main.js) β
β β
β OnTheFly.setUI({ β
β type: "Column", children: [ β
β { type: "Text", props: { text: "Hello" } }, β
β { type: "Button", props: { text: "Tap", onClick: "f" }}β
β ] β
β }); β
β β β² β
β βΌ β β
β βββββββββββ βββββββββββ βββββββββββ β
β β QuickJS βββββΆβ JNI βββββΆβ Compose ββββΆ Native UI β
β β (C) β β Bridge β β Render β β
β βββββββββββ βββββββββββ βββββββββββ β
β β β
β User taps button β
β β β
β onClick("f") β JS β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
JavaScript defines the UI structure and logic. QuickJS executes it natively (no WebView). The JNI bridge passes the UI tree to Kotlin, where Jetpack Compose renders real native widgets. User interactions flow back to JS.
| Feature | Description |
|---|---|
| Dynamic UI | Define UI in JavaScript, rendered as native Compose widgets |
| Hot Reload | Edit JS β save β app auto-reloads in ~2 seconds |
| Navigation | Multi-screen navigation driven entirely by JS |
| API Calls | JS requests HTTP calls, native Ktor client executes, response returns to JS |
| Popups | Full-screen overlays and confirm dialogs, controlled from JS |
| Event System | Lifecycle, data, component events β bidirectional bridge |
| Targeted Updates | Update only changed nodes, not the entire UI tree |
| Style System | Centralized themes in theme.js, components reference by name |
| Timber Logging | OnTheFly.log.d/i/w/e() in JS β Timber in Logcat |
| Dev Server | Python server with file watcher, validator, deploy commands |
| Script Validator | Check JS syntax from terminal before deploying |
graph TB
subgraph "Presentation"
MA[MainActivity]
SP[SplashScreen]
SS[ScriptScreen]
DR[DynamicRenderer]
end
subgraph "ViewModel"
VM[ScriptViewModel]
end
subgraph "Domain"
UC[LoadScriptUseCase]
EE[EngineEvent]
NA[NativeAction]
end
subgraph "Data"
LS[LocalScriptStorage]
UM[ScriptUpdateManager]
NS[NetworkSource - Ktor]
DS[DevServerSource]
end
subgraph "Engine"
QJS[QuickJSEngine]
STY[StyleRegistry]
BR[bridge.cpp - JNI]
QC[QuickJS C Library]
end
MA --> SP
SP --> SS
SS --> VM
SS --> DR
VM --> UC
VM --> QJS
VM --> UM
VM --> NS
UM --> DS
UM --> LS
UC --> LS
QJS --> STY
QJS --> BR
BR --> QC
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β FIRST LAUNCH β
β assets/scripts/ ββcopyβββΆ files/scripts/ (local storage) β
β β
β RUNTIME β
β Always load from files/scripts/ (local) β
β β
β UPDATE (Dev) β
β Dev Server ββdownloadβββΆ files/scripts/ βββΆ auto-reload β
β β
β UPDATE (Production) β
β Remote CDN ββdownloadβββΆ files/scripts/ βββΆ reload β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
OnTheFly-Android/
βββ devserver/ # Dev tools
β βββ scripts/ # Source of truth for all JS bundles
β β βββ version.json # Version manifest
β β βββ home/ # Home screen
β β β βββ manifest.json
β β β βββ main.js
β β β βββ theme.js
β β βββ demo-app/ # Demo: navigation, popups, events
β β βββ detail-app/ # Demo: data passing
β β βββ api-demo/ # Demo: HTTP API calls
β β βββ popup-fullscreen/ # Demo: full screen popups
β β βββ popup-confirm/ # Demo: confirm dialogs
β βββ server.py # Dev server + validator + deploy
β βββ start.sh # Quick start script
β
βββ app/src/main/
β βββ assets/scripts/ # Build artifact (auto-copied from devserver/)
β βββ cpp/ # Native C/C++ layer
β β βββ quickjs/ # QuickJS engine source
β β βββ bridge.cpp # JNI bridge
β β βββ CMakeLists.txt
β βββ java/com/dongnh/onthefly/
β βββ engine/ # QuickJS wrapper + style registry
β βββ domain/ # Models, use cases, interfaces
β βββ data/ # Local storage, network, dev server
β βββ presentation/ # Screens, renderer, navigation, viewmodel
β βββ ui/theme/ # Material3 theme
β βββ OnTheFlyApp.kt # Application class (init storage)
β βββ MainActivity.kt
β
βββ docs/images/ # Screenshots
βββ README.md
Interactive dev server with file watcher, validator, and deploy:
# Start (auto-setup venv on first run)
./devserver/start.sh OnTheFly Dev Server
βββββββββββββββββββββββββββββββββββββββ
Port: 8080
Bundles: home, demo-app, api-demo, ...
Emulator: http://10.0.2.2:8080
βββββββββββββββββββββββββββββββββββββββ
β File watcher active (watchdog)
β HTTP server on port 8080
βββββββββββββββββββββββββββββββββββββββ
onthefly> _
| Command | Description |
|---|---|
v, validate [bundle] |
Check JS syntax errors |
d, deploy [bundle] |
Copy scripts β Android assets |
l, list |
List all bundles |
r, reload |
Force app reload |
h, help |
Show commands |
q, quit |
Stop server |
python server.py # Start server
python server.py validate # Validate all bundles
python server.py validate home # Validate one bundle
python server.py deploy # Deploy all β Android assets
python server.py deploy home # Deploy one bundlesequenceDiagram
participant Dev as Developer
participant FS as File System
participant WD as Watchdog
participant SRV as Dev Server
participant APP as Android App
Dev->>FS: Save main.js
FS->>WD: File change detected
WD->>SRV: Bump globalVersion
APP->>SRV: Poll /version (every 2s)
SRV-->>APP: New globalVersion
APP->>SRV: Download changed files
SRV-->>APP: main.js, theme.js
APP->>APP: Save to local storage
APP->>APP: Reload engine + re-render UI
Each screen is a bundle β a folder with manifest, theme, and entry script:
home/
βββ manifest.json # { "name": "Home", "version": "1.0.0", "entry": "main.js" }
βββ theme.js # Centralized styles
βββ main.js # UI + logic
function onCreateView() {
OnTheFly.log.i("Screen loaded");
}
function render() {
OnTheFly.setUI({
type: "Column",
props: { style: "container" },
children: [
{ type: "Text", props: { text: "Hello", style: "title" } },
{ type: "Button", props: { text: "Next", onClick: "goNext", style: "primary" } }
]
});
}
function goNext() {
OnTheFly.sendToNative("navigate", { screen: "detail", data: { id: 1 } });
}
render();OnTheFly.registerStyles({
title: { fontSize: 28, fontWeight: "bold", color: "#1A1A2E" },
primary: { backgroundColor: "#0F3460", color: "#FFF", cornerRadius: 12 },
container: { padding: 24, spacing: 16, alignment: "center" }
});| Component | Props | Description |
|---|---|---|
Column |
padding, spacing, alignment, style |
Vertical layout |
Row |
spacing, alignment, style |
Horizontal layout |
Text |
text, style, id |
Text display |
Button |
text, onClick, id, style |
Clickable button |
Toggle |
id, label, checked |
Switch/toggle |
Spacer |
height, style |
Empty space |
FullScreenPopup |
id, visible, onDismiss, style |
Full screen overlay |
ConfirmDialog |
id, visible, title, message, onConfirm, onCancel |
Alert dialog |
| Method | Description |
|---|---|
OnTheFly.setUI(tree) |
Set full UI tree |
OnTheFly.update(id, props) |
Targeted update (fast) |
OnTheFly.registerStyles(styles) |
Register named styles |
OnTheFly.sendToNative(action, data) |
Navigate, API call, toast, goBack |
OnTheFly.log(msg) |
Log to Timber (INFO) |
OnTheFly.log.d/i/w/e(msg) |
Log with level |
console.log(msg) |
Log to Timber (DEBUG) |
OnTheFly.sendToNative("navigate", { screen: "detail", data: {...} });
OnTheFly.sendToNative("goBack");
OnTheFly.sendToNative("showToast", { message: "Hello!" });
OnTheFly.sendToNative("sendRequest", { id: "r1", url: "...", method: "GET" });function onCreateView() { } // Screen created
function onResume() { } // App foreground
function onPause() { } // App background
function onDestroy() { } // Screen destroyed
function onVisible() { } // Screen re-entered
function onBackPressed() { } // Back button
function onDataReceived(data) { } // API response
function onViewData(data) { } // Data from navigation| Component | Technology |
|---|---|
| UI | Jetpack Compose + Material3 |
| Script Engine | QuickJS (v2025-09-13, ~210KB) |
| Native Bridge | JNI via NDK + CMake |
| HTTP Client | Ktor Client + OkHttp |
| Navigation | Compose Navigation |
| Logging | Timber |
| Architecture | Clean Architecture |
| Language | Kotlin + C/C++ |
| Dev Server | Python + watchdog |
| Min SDK | 24 (Android 7.0) |
| Target SDK | 35 |
# Build
./gradlew assembleDebug
# Install
./gradlew installDebug
# With hot-reload
./devserver/start.sh # Terminal 1
./gradlew installDebug # Terminal 2
# Edit devserver/scripts/home/main.js β app auto-reloadsMIT


