Android
Meratyn runs as a native Android app built from the same React codebase via Tauri v2 mobile — no separate app. The webview UI is bundled in the APK, so the app shell works offline; map tiles and the heavier engines are fetched on demand (same as the desktop build).
What works on Android vs desktop
The Android build ships the full map workspace, Add Data, the Vector tools (Turf.js / in-browser GeoPandas via Pyodide), the SQL Workspace (DuckDB-WASM and the in-browser PGlite/PostGIS engine), the Python Console (Pyodide), geocoding, statistics, the AI assistant, story maps, and plugins.
Android has no Python sidecar or local helper binaries. Browser-capable
processing runs on the device where the operation and input format are
supported: Whitebox and raster tools use their WASM engines, and browser
conversion formats remain available. Sidecar-only tools, native-file paths, and
the local PostgreSQL/Martin source stay hidden on mobile. These limits are
gated by a user-agent isMobile() check so an unavailable path does not appear
and then fail.
Toolchain setup (one time)
You need the Android SDK + NDK, a JDK (17 or 21 — newer JDKs can break the
Android Gradle Plugin), and the Rust Android targets. The cleanest, sudo-free
layout keeps everything under a user-writable SDK at ~/Android/Sdk.
# 1. JDK 17/21 (or reuse Android Studio's bundled JBR at /opt/android-studio/jbr)
export JAVA_HOME=/path/to/jdk-21
# 2. Android SDK components (sdkmanager ships with Android Studio cmdline-tools)
export ANDROID_HOME="$HOME/Android/Sdk"
yes | sdkmanager --sdk_root="$ANDROID_HOME" --licenses
sdkmanager --sdk_root="$ANDROID_HOME" \
"platform-tools" "platforms;android-36" \
"build-tools;36.0.0" "ndk;27.3.13750724"
export NDK_HOME="$ANDROID_HOME/ndk/27.3.13750724" # Tauri needs NDK_HOME
# 3. Rust + the four Android targets (install rustup if you don't have it)
rustup target add aarch64-linux-android armv7-linux-androideabi \
i686-linux-android x86_64-linux-android
NDK r27 (LTS) is the supported line for Tauri v2. Add the four exports to
your shell profile so every session has them.
API 36 (Android 16), not 34: the Tauri v2.11 Android template generates
compileSdk = 36 / targetSdk = 36, so Gradle needs the matching platform
installed. It also matches the Android platform floor — from 2026-08-31,
apps targeting Android are expected to declare API 36 or newer.
Apps targeting Android 15+ whose native libraries are not aligned for 16 KB memory pages fail to load on 16 KB devices. NDK r28+ does this by default; r27 does not, so the flags are passed explicitly. Export this before an Android build:
export RUSTFLAGS="-C link-arg=-Wl,-z,max-page-size=16384 -C link-arg=-Wl,-z,common-page-size=16384"
It has to be the
RUSTFLAGSenvironment variable. Putting the same flags intarget.<triple>.rustflagsin a.cargo/config.tomldoes not work: the Tauri CLI setsRUSTFLAGSitself when it invokes cargo for Android, and an envRUSTFLAGSoverrides the config file outright. The config-file form is silently ignored — it parses, it builds, and it ships 4 KB-aligned libraries that fail to load on 16 KB devices. Tauri appends to an inherited value, so exporting it works.
Check the result on a built APK:
unzip -o -q app-arm64-release-unsigned.apk 'lib/*/*.so' -d /tmp/apkcheck
"$NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-objdump" -p \
/tmp/apkcheck/lib/*/*.so | awk '$1 == "LOAD" { print $NF }' | sort -u
# every value must be 2**14 or greater; 2**12 means the flags did not apply
Use llvm-objdump -p rather than readelf -l: readelf wraps each LOAD across
two lines, so it is easy to parse the wrong column and read an address as an
alignment. Run this check on every build whose native libraries changed,
before installing anything on a 16 KB device.
Build
cd apps/mappview
bunx --no-install tauri android init # generate src-tauri/gen/android (once)
bunx --no-install tauri android build --apk --split-per-abi # release APKs, one per ABI (~40 MB each)
bunx --no-install tauri android build --aab # only if a bundle (.aab) is ever needed
-
gen/androidis generated (git-ignored) and regenerated on demand. -
Build release, not
--debug: the stripped, size-optimized Cargo profile makes each APK ~40 MB; a debug build is ~200 MB (unstripped.sowith debuginfo). -
--split-per-abiemits one APK per architecture instead of a single ~150 MB universal APK. Install thearm64one on real phones. -
Output:
src-tauri/gen/android/app/build/outputs/apk/<abi>/release/app-<abi>-release-unsigned.apk, where<abi>is Tauri’s short name —arm64,arm,x86,x86_64— not the Android ABI directory name (arm64-v8a,armeabi-v7a) that appears inside the APK underlib/. -
Sideload path: the per-ABI APKs — the installable artifacts.
The app is named Meratyn on Android (the desktop build is “Meratyn
Desktop”) and uses the package id com.mapptech.mappview, both set via
src-tauri/tauri.android.conf.json, which also drops the Python backend from
the Android bundle.
The Android id is overridden there rather than in tauri.conf.json on purpose:
identifier is shared by every platform and also determines desktop application
data and configuration directories. The desktop value remains the legacy
com.mapptech.mappview identifier so Meratyn upgrades keep existing plugins,
notebooks, managed runtimes, settings, and persisted filesystem scopes.
Signing
Release APKs are unsigned. To install one, sign it (a debug key is fine for testing; use a real key for distribution):
BT="$ANDROID_HOME/build-tools/36.0.0"
KS="$HOME/.android/debug.keystore" # auto-created by Android tooling; or make your own
# -P 16, not -p: -p only guarantees 4 KB; the .so must sit on a 16 KB boundary
# inside the zip to load on 16 KB devices.
"$BT/zipalign" -P 16 -f 4 app-arm64-release-unsigned.apk aligned.apk
"$BT/apksigner" sign --ks "$KS" --ks-pass pass:android \
--ks-key-alias androiddebugkey --key-pass pass:android \
--out mappview-arm64.apk aligned.apk
"$BT/apksigner" verify mappview-arm64.apk
The example signs the arm64 APK. Substitute the ABI you need throughout —
app-x86_64-release-unsigned.apk → mappview-x86_64.apk, and likewise for
armeabi-v7a / x86. The emulator section below installs mappview-x86_64.apk,
which is this same walkthrough with arm64 swapped for x86_64.
For a real upload/release key:
keytool -genkeypair -v -keystore upload.jks -alias upload -keyalg RSA \
-keysize 2048 -validity 10000
Release builds
Android builds are local-only. Configure a Meratyn-owned upload keystore, then
run the Tauri Android build on a trusted development machine. Keep the keystore
and passwords outside the repository. The .apk is the installable artifact
for local testing and sideloading.
Install / test
On a phone
- Enable Developer options (tap Build number 7×) and USB debugging.
- Sideload the signed APK:
Or copy the APK to the phone and tap it (allow “install unknown apps”).adb install -r mappview-arm64.apk
For live development with hot reload, connect the device and run
bun run tauri android dev.
On an emulator
sdkmanager --sdk_root="$ANDROID_HOME" \
"emulator" "system-images;android-36;google_apis_playstore;x86_64"
avdmanager create avd -n mappview \
-k "system-images;android-36;google_apis_playstore;x86_64" -d pixel_7
emulator -avd mappview
# x86_64 APK to match the x86_64 system image — the emulator can translate the
# arm64 build, but far slower, and it would not exercise the x86_64 libraries.
adb install -r mappview-x86_64.apk
If you ever rebuild with a different signing key, uninstall the old copy first (
adb uninstall com.mapptech.mappview) — Android rejects updates whose signature changed.
Known limitations / follow-ups
- Local-file sources (MBTiles, local rasters, project files) assume real filesystem paths; Android scoped storage returns content URIs, so those flows need adapting before they work natively.
- The Download Offline Area tool relies on a service worker, which the Tauri builds (desktop and Android) don’t use — it’s a PWA feature. Native offline basemap caching (bundled/downloaded MBTiles/PMTiles) is a future enhancement.
- Earth Engine OAuth uses a desktop loopback/multi-window flow; a mobile deep-link redirect is future work.