dx build --platform android --package mobile generates a Gradle project under target/dx/mobile/<debug|release>/android/app/ from its own templates (Kotlin MainActivity, AndroidManifest.xml with INTERNET already granted, build.gradle.kts), compiles the Rust crate as a shared library for each requested ABI into app/src/main/jniLibs/<abi>/, copies assets, and runs ./gradlew to produce:
[application] name is the launcher label; [bundle] identifier (io.github.mathstruct.moonkale) is the package id; [android] min_sdk = 24 (dx’s default) / target_sdk = 35.
Toolchain (Arch)
# JDK 17 (the generated Gradle project targets JVM 17)sudo pacman -S jdk17-openjdk# Android SDK + NDK: either Android Studio (AUR: android-studio) and its SDK Manager,# or the command-line tools (AUR: android-sdk-cmdline-tools-latest) and:sdkmanager "platform-tools" "platforms;android-35" "build-tools;35.0.0" "ndk;27.2.12479018"# Rust targetsrustup target add aarch64-linux-android # phonesrustup target add x86_64-linux-android # emulator
cd packages/mobileadb devices # a device with USB debugging, or a running emulatordx serve --platform android # builds, installs, launches; hot-reloads rsx/assets
dx uses adb reverse so the app reaches the dev server on 127.0.0.1 (Debugging and Logging for adb logcat).
What actually worked (2026-09-19, Galaxy S10e / Android 13, NDK 29.0.14206865)
dx build --release --platform android --features mobile --target aarch64-linux-android then adb install -r target/dx/mobile/release/android/app/app/build/outputs/apk/debug/app-debug.apk. The plain debug build is x86_64 (INSTALL_FAILED_NO_MATCHING_ABIS) and too large for a phone with 1.4 GB free (INSTALL_FAILED_INSUFFICIENT_STORAGE); the release APK is 23 MB. Launch with adb shell monkey -p io.github.mathstruct.moonkale -c android.intent.category.LAUNCHER 1; inspect with adb exec-out screencap -p, adb forward tcp:9222 localabstract:webview_devtools_remote_<pid> (WebView DevTools) and adb shell run-as io.github.mathstruct.moonkale. The five WebView-specific fixes are P-087–P-091 in the Problem Log; the recipe lives in packages/mobile/README.md.
Fill [android.signing] in packages/mobile/Dioxus.toml — but do not commit passwords. Two workable patterns:
CI writes the block into Dioxus.toml from secrets right before dx bundle (the file is not committed in that state);
locally, keep a .gitignored copy Dioxus.toml and a committed Dioxus.toml.example (dx reads only Dioxus.toml, it has no env-var substitution as of 0.7.10).
Build:
cd packages/mobiledx bundle --release --platform android --package-types apk # sideload / GitHub releasedx bundle --release --platform android --package-types aab # Google Play
Without [android.signing], a release build still produces the debug-signed APK path; dx only switches to app-release.apk when signing is configured (android_apk_path() in the CLI).
Verify: apksigner verify --print-certs app-release.apk, then adb install -r app-release.apk.
Distribution channels
channel
needs
notes
GitHub Releases (sideload)
signed APK
simplest; users enable unknown sources
Google Play
AAB, Play Console account, target_sdk per current Play policy
dx produces the AAB; upload manually or with fastlane supply
F-Droid
reproducible build from source on their server
Rust + NDK builds are supported via srclibs; needs a metadata YAML and no proprietary deps. Blocked on: an icon, a tagged release, and confirming the Gradle project dx generates builds offline
Name and icon (spec 007)
dx 0.7.10 labels the app after the crate (Mobile) and ships its own launcher icon; packages/mobile/build-android.sh fixes both after the dx build (writes strings.xml, generates the launcher and adaptive icons from assets/icon.png) and reassembles with Gradle. Use it instead of a bare dx build for anything that leaves the desk.
System bars (Milestone 16): the same script writes a dark AppTheme (status bar, navigation bar and window background #0b0d12, the title bar’s colour) instead of dx’s AppCompat Light template, which left a grey strip above the app. Android 15’s edge-to-edge enforcement (target 35) would ignore these colours; the opt-out needs compileSdk 35, dx’s template uses 34 — open, untested.
Extensions, app size and the stores
Android Extensions and Bundling answers what can be added after install (wasm extensions from a URL, once the phone has a runtime and an installer — never native code), how small the APK can get (strip + a clean asset dir ≈ 15 MB, feature-trimmed ≈ 12 MB), and what Google Play and F-Droid require. Before a release build, delete target/dx/mobile/release/android/app/app/src/main/assets — dx keeps every previous hashed asset (P-092).
What Moonkale needs before Android is useful
Folder access: open_local uses FolderSource::open("."), which on Android is the app sandbox. Real folders need the Storage Access Framework (R-25 in Problem Ranking).
Layout: the workbench is desktop-shaped; the collapsed mobile shell is Phase 5.
the APK installs and opens on a device; log the result in Problem Log
Signing
The release APK is signed with a keystore whose base64 lives in the ANDROID_KEYSTORE_BASE64 repository secret; packages/mobile/build-android.sh takes it through MOONKALE_ANDROID_KEYSTORE and builds assembleRelease. Why it matters, how to generate the key and what the one-time switch costs users: packages/mobile/README.md and Problem Log P-131.