Runs iced apps on Android with iced's normal runtime: iced::application or iced::daemon, Task, Subscription, the same code as on desktop.
It has two parts:
- the iced fork (branch
android-0.14), which runs iced's winit shell on Android and adds touch support to some widgets - this crate, which connects iced to the parts of Android that only Java can reach, through
IcedActivity(aNativeActivitysubclass injava/)
A complete app is the runtime example (see Example below), here on a Pixel 4a:
In use in Syncative, a file sync app.
- soft keyboard: predictions, autocorrect, swipe typing and capitalisation, with the text around the cursor passed to the keyboard (password fields pass it masked); the word being typed goes straight into the text; password and terminal modes
- [
scroll_to_focused()]: scrolls the focused text field into view when the keyboard opens - long press in
text_inputandtext_editorselects a word and opens Android's Cut/Copy/Paste/Select all menu; dragging after the long press extends the selection text_editor: tap to place the cursor, drag to scrollscrollable: fling (it keeps scrolling after a swipe), and swipes that start on a button scroll instead of pressing it; scrolling keeps the focused field and the keyboardtooltip: opens after a long press and closes on lift, without pressing the button underneathpick_list: opens on lift, so swipes that start on it scroll- clipboard (
clipboard::read/write) - insets: the app draws behind the status bar, navigation bar, display cutout and keyboard and pads its content by [
insets()] - [
system_scale()]: the system's font size and display size, which iced does not follow by itself (winit keeps the density the process started with); scale by it, e.g. withscale_factor - light/dark system-bar icons, system dark mode (
system::theme,theme_changes), back gesture (asKey::Named(BrowserBack)) - pause/resume (Home, app switcher) and rotation without restarting the app
- [
foreground()]: tells the app when it can no longer be seen, so it can close its window, which frees iced's GPU resources, and open it again on return (see the example) - the app keeps running when Android destroys the activity (e.g. swiped away from the recent apps while a foreground service keeps the process alive); the next activity attaches to it instead of crashing with winit's
RecreationAttempt. This needs the android-activity fork below - fonts from
/system/fonts, since iced finds system fonts only through fontconfig; mapped from the files, not copied to the heap, and their pages released from the app's memory while it is in the background (13 MB less on a Pixel 4a with Android 13); colour emoji on Android 15+ with theemojifeature (see below)
Tested on Android 8.0 (emulator), 13 (Pixel 4a) and 17 (Pixel 7; emulator with 16 KB pages); details below.
Cargo.toml. Use iced from the fork; every iced crate must come from the same place. Each release of this crate is tagged vX.Y.Z here and iced_android-vX.Y.Z in both forks; use the tags of the same release everywhere, or Cargo sees two different iced:
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
iced = { git = "https://github.com/Santuzius/iced", tag = "iced_android-v0.1.0", features = ["x11"] }
iced_android = { git = "https://github.com/Santuzius/iced_android", tag = "v0.1.0" }Also use the android-activity fork (branch process-lifetime-0.6), which lets android_main outlive a destroyed activity (iced_android::init turns this on). winit allows only one event loop per process (winit#3325), so without it a second activity in the same process cannot start iced again:
[patch.crates-io]
android-activity = { git = "https://github.com/Santuzius/android-activity", tag = "iced_android-v0.1.0" }x11 (or wayland) only satisfies iced's check that Unix targets choose a display server; nothing of it is built for Android. If other dependencies use iced from crates.io, redirect them to the fork too:
[patch.crates-io]
iced = { git = "https://github.com/Santuzius/iced", tag = "iced_android-v0.1.0" }
iced_core = { git = "https://github.com/Santuzius/iced", tag = "iced_android-v0.1.0" }
# … every iced_* crate they useRust. Start the app from android_main, and use the crate's helpers. They do nothing on other platforms, so no cfg is needed:
#[cfg(target_os = "android")]
#[unsafe(no_mangle)]
fn android_main(app: iced_android::AndroidApp) {
iced_android::init(app);
let _ = run();
}
pub fn run() -> iced::Result {
let mut app = iced::application(App::new, App::update, App::view)
.subscription(App::subscription)
.default_font(iced_android::DEFAULT_FONT);
for font in iced_android::fonts() {
app = app.font(font);
}
app.run()
}
// In the subscription: iced_android::insets().map(Message::Insets)
// In the view: container(content).padding(self.insets)
// On Message::Insets, if insets.bottom grew (keyboard opened): return iced_android::scroll_to_focused()
// On theme changes: iced_android::set_system_bars_dark(is_dark)
// On Back at the top level: iced_android::move_to_background()
// In the subscription: iced_android::foreground().map(Message::Foreground); on false close the window, on true open it againGradle. Compile IcedActivity from wherever Cargo checked the crate out (app/build.gradle):
android {
sourceSets.main.java.srcDirs += icedAndroidJava()
}
def icedAndroidJava() {
def metadata = providers.exec {
commandLine 'cargo', 'metadata', '--format-version', '1', '--manifest-path', "${rootDir}/Cargo.toml"
}.standardOutput.asText.get()
def crate = new groovy.json.JsonSlurper().parseText(metadata).packages.find { it.name == 'iced_android' }
return new File(crate.manifest_path).parentFile.toPath().resolve('java').toString()
}AndroidManifest.xml. Use IcedActivity (or a subclass) with the library name and these attributes:
<application android:enableOnBackInvokedCallback="true" android:theme="@android:style/Theme.DeviceDefault.NoActionBar" …>
<activity
android:name="io.github.santuzius.icedandroid.IcedActivity"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|density|keyboard|keyboardHidden|navigation|uiMode|locale|layoutDirection|fontScale"
android:windowSoftInputMode="adjustResize|stateHidden"
android:exported="true">
<!-- intent-filter MAIN/LAUNCHER -->
<meta-data android:name="android.app.lib_name" android:value="your_lib_name" />
</activity>
</application>configChanges keeps rotation and dark mode from restarting the activity, which would restart android_main.
iced draws bitmap (CBDT) and COLRv0 colour fonts, but not COLRv1. Up to Android 14 the system has a bitmap emoji font; Android 15+ ships its emoji only as COLRv1. The emoji feature bundles Mozilla's Twemoji font (COLRv0, 1.5 MB) and loads it only when the system has no usable emoji font:
iced_android = { git = "https://github.com/Santuzius/iced_android", tag = "v0.1.0", features = ["emoji"] }Its art is licensed CC-BY 4.0, which requires credit: show "Twemoji by Twitter, CC-BY 4.0" somewhere in the app, e.g. in an About screen. Without the feature, emoji on Android 15+ show as boxes or in black and white; ⚠ ✓ ✗ → ↻ work everywhere in any case. ⟳ (U+27F3) is in no Android system font.
examples/runtime is one iced app (iced::daemon) for desktop and Android, with three pages that exercise what Syncative needs:
- runtime:
Task::perform,Subscription::runwithstream::channelfed from a worker thread,time::every,window::close_events,system::themeandtheme_changes,keyboard::listen,clipboard::read/write - widgets:
button,container,text_input(also secure),text_editor,tooltip,pick_list,svg(Tabler icons viaicondata, gradients),scrollable,opaque+stackmodal,center,toggler,rich_text,mouse_area - Android: everything under What works
Files:
src/app.rs: the iced app, platform-independent;cargo run --bin desktopruns it on Linuxsrc/lib.rs:android_main, which hands over toiced_androidapp/build.gradle,app/src/main/AndroidManifest.xml: the setup described above, withiced_androidas a path dependency
Build and run:
nix-shell # in the repository root: Android SDK, NDK, emulator, JDK, Gradle (NixOS); elsewhere set ANDROID_HOME and ANDROID_NDK_HOME yourself
rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android
cd examples/runtime
./build.sh # all ABIs, release; ./build.sh debug x86_64 for the emulator only
adb install -r app/build/outputs/apk/release/app-release.apk
adb logcat -s iced-runtime # Rust log and panicsEmulators (x86_64 images for API 26, 33, 36 and 37.0 are in shell.nix):
avdmanager create avd -n api26 -k "system-images;android-26;google_apis;x86_64" -d pixel_4a
avdmanager create avd -n api37 -k "system-images;android-37.0;google_apis_ps16k;x86_64" -d pixel_4a
emulator -avd api26 -gpu swiftshader_indirectTo work on the iced fork locally, point Cargo at a local clone in examples/runtime/.cargo/config.toml (gitignored):
[patch."https://github.com/Santuzius/iced"]
iced = { path = "../../../iced" }
iced_core = { path = "../../../iced/core" }
iced_futures = { path = "../../../iced/futures" }
iced_runtime = { path = "../../../iced/runtime" }
iced_widget = { path = "../../../iced/widget" }
iced_winit = { path = "../../../iced/winit" }Tested:
| Device | Android | Graphics | Result |
|---|---|---|---|
| Pixel 4a | 13 (GrapheneOS), arm64 | wgpu, Vulkan | everything above |
| Pixel 7 | 17, arm64 | wgpu, Vulkan | everything above, in daily use with Syncative |
| Emulator API 26 | 8.0, x86_64 | wgpu, OpenGL ES | starts, keyboard and typing, fonts (an earlier version) |
| Emulator API 37.0 | 17, x86_64, 16 KB pages | wgpu, OpenGL ES | starts, Task, keyboard and typing (an earlier version); colour emoji with Twemoji |
| Galaxy A3 (2017) | 8.0, armv7 | – | not tested; the APK contains armeabi-v7a |
- No selection handles (the drag markers at both ends of a selection): after a long press, the selection can only be extended by dragging before lifting.
mouse_areafireson_presson touch-down, so a swipe that starts on it still presses it; useon_releasefor touch-friendly areas.
Pull requests are welcome, including ones written with AI. I maintain this alongside Syncative, its main user, and have little time for it, so don't expect quick replies. I test each pull request on my own phones; if it doesn't break anything in Syncative and my AI agent's review finds it OK, I merge it and tag a new release.
This crate and the fork's Android changes were written with the help of AI (Claude), and tested by hand on the devices above.
The iced team does not maintain mobile support (iced#302), so please report problems here, not to iced.
This started as a fork of ibaryshnikov/android-iced-example (MIT). The example's Gradle and manifest skeleton and its launcher icons come from there, and the Rust ↔ Java calls for keyboard and clipboard follow its approach; hence its copyright line in LICENSE.
MIT, except the bundled Twemoji font in fonts/: Apache 2.0 (font) and CC-BY 4.0 (art), see fonts/LICENSE-Twemoji.md.



