Android
Wails v3 applications run on Android as native apps: an Android WebView
renders the frontend, assets are served in-process through a
WebViewAssetLoader backed by the Go asset server (no localhost server, no
open ports), and the standard @wailsio/runtime works unchanged — service
bindings, events, dialogs and the clipboard route through the Go message
processor.
The same main.go builds for desktop and Android. The Go code is compiled as
a C shared library (libwails.so, GOOS=android + the NDK toolchain) and
loaded by a small Java host. Android-specific behaviour lives in per-platform
Go files guarded by //go:build android.
Requirements
- The Android SDK with platform-tools, an SDK platform (API 35),
build-tools and the NDK (26.3.x) —
wails3 doctorshows what it finds - A JDK (e.g. OpenJDK 21) for Gradle; set
JAVA_HOMEifjavais not on yourPATH - Go 1.25+ and npm
ANDROID_HOME(orANDROID_SDK_ROOT) pointing at the SDK
Install the SDK pieces with the command-line tools:
sdkmanager "platform-tools" "platforms;android-35" "build-tools;35.0.0" \
"ndk;26.3.11579264" "emulator" \
"system-images;android-35;google_apis;arm64-v8a"
avdmanager create avd --name wails \
--package "system-images;android-35;google_apis;arm64-v8a" \
--device pixel_7
Running on the Emulator
From your project directory:
wails3 task android:run
This boots an emulator if none is running, generates the bindings, builds the
frontend, compiles your Go code to libwails.so for the emulator’s ABI,
assembles a debug APK with Gradle, then installs and launches it.
Useful companions:
wails3 task android:logs # stream the app's logcat output
In debug builds the WebView is inspectable from Chrome at chrome://inspect.
Packaging
wails3 task android:package # production release APK
wails3 task android:deploy-emulator # install + launch it
wails3 task android:bundle # production release AAB (Android App Bundle)
wails3 task android:bundle:fat # release AAB containing all ABIs
wails3 task android:run:device # debug install + launch on a physical device
wails3 task android:deploy-device # install + launch on a physical device
DEVICE_ID=<serial> wails3 task android:run:device
DEVICE_ID=<serial> wails3 task android:deploy-device
Production builds use -tags production,android, are stripped, and compile
out the framework’s internal diagnostics. wails3 task android:package:fat
builds both arm64-v8a and x86_64 into a single APK.
Google Play requires the Android App Bundle (.aab) format for new app
submissions, and new submissions must target Android 15 (API 35) or higher;
the project template sets compileSdk and targetSdk to 35 in
build/android/app/build.gradle. wails3 task android:bundle:fat produces
bin/<AppName>.aab with both ABIs included; Google Play generates
optimised per-device APKs from it, so the fat bundle is the right artifact
for store uploads. APKs remain the quickest route for local and emulator
testing, because an .aab cannot be installed directly with adb.
android:run and android:deploy-emulator are emulator-oriented tasks. For a
physical Android device, use android:run:device for a debug APK or
android:deploy-device for a release APK. Both build for arm64, select the
first connected non-emulator entry from adb devices, install it, and launch
com.wails.app.MainActivity. Pass DEVICE_ID=<serial> to target a specific
device.
Signing & release builds
Without a keystore, release builds are signed with the Android debug keystore so they install for testing. To sign with your own keystore, set:
ANDROID_KEYSTORE_FILE=/path/to/release.jks \
ANDROID_KEYSTORE_PASSWORD=... \
ANDROID_KEY_ALIAS=... \
ANDROID_KEY_PASSWORD=... \
wails3 task android:package
The same variables sign App Bundles: run wails3 task android:bundle:fat
with them set to produce a Play-ready .aab. Without them the bundle is
signed with the debug keystore and Google Play will reject it, so the task
prints a warning.
Configuration
The frontend drives Android features at runtime through the Android runtime
object: Android.Haptics.Vibrate(durationMs), Android.Device.Info(),
Android.Toast.Show(message). The package name is controlled by APP_ID in
the build tasks.
What works, what doesn’t
| Area | Status |
|---|---|
WebView + in-process assets (WebViewAssetLoader) |
✅ |
| Service bindings, events (both directions) | ✅ |
| Message dialogs | ✅ AlertDialog with button callbacks |
| Open file / files dialogs | ✅ Storage Access Framework (files imported as cache copies) |
| Open directory / save file dialogs | ❌ Return an error — write inside the app sandbox instead |
| Clipboard | ✅ ClipboardManager |
| Screens API | ✅ WindowMetrics incl. system-bar work area |
Lifecycle events (events.Android.*) |
✅ |
| Haptics, device info, toast | ✅ Android.* runtime API |
| Emulator + physical device builds | ✅ android:run, android:run:device, android:deploy-emulator, android:deploy-device |
| Window geometry, menus, system tray | Intentional no-ops |
| Multiple windows | Only the first window is displayed |
Porting notes
- Desktop code compiles unchanged under
GOOS=android; geometry/menu/tray calls become no-ops because Android apps are fullscreen. androidimplies thelinuxbuild tag (Android is a Linux kernel): desktop-Linux-only files need//go:build linux && !android, and at runtimeruntime.GOOSis"android".- Replace save-file and choose-directory dialogs with writes into the app sandbox plus an intent share flow. Open-file dialogs work and import the chosen documents as cache-directory copies, so you get real filesystem paths.
- A real app is always built with
CGO_ENABLED=1and the NDK; the non-cgo path exists only so tooling such aswails3 generate bindingscan load the package. - Design the frontend responsively; the
Screenswork area excludes the status and navigation bars.