Android (Material Components)
Tier 1 · Supportedandroid-mdc is a Tier 1 target for Android apps. Its native integration uses Android views
and Material Components through JNI, with a Gradle host project. Check permissions and input
behavior on the Android devices your app needs to serve.
Getting started
You need the Android SDK and NDK, JDK 17 or newer, and cargo-ndk;
day doctor --toolkit android checks each one and prints what’s missing. To work in Android Studio,
you also need Android Studio 2026.1.4 or newer
(system requirements).
cargo install --git https://github.com/daybrite/day day-cli
cargo install cargo-ndk
day new app my-app --toolkit android-mdc
cd my-app
day launch -t android-mdc # installs on every connected device/emulator, right ABI each
day pack -t android-mdc # release-signed .apk + .aab
The Gradle project
day build, day prepare, and day open derive a standard Gradle project with short
scripts under build/android/. No checked-in Android build scaffold is required. An existing
platform/android/ project takes precedence. Day’s Gradle plugin, applied in
settings.gradle.kts and app/build.gradle.kts, configures the app module from Day.toml and your
pieces: the application id and version, the SDK levels, the Android Gradle Plugin version, source
folders, libraries, the merged manifest, release minification, and signing. day build,
day prepare, and day open put the plugin in place from the version of Day your app builds
against, so updating Day updates your Android build and these files stay as they are.
Common native customizations belong in Day.toml:
[android]
cleartext-domains = ["127.0.0.1"] # HTTP for these exact hosts; HTTPS elsewhere
# cleartext-traffic = true # apps intentionally accepting HTTP from any host
edge-to-edge = true
resource-overlays = ["platform/android/res"]
# optimize = true # let R8 optimize the release build, not only shrink it
Release builds shrink and rename with R8. It keeps the Java classes and methods your Rust calls,
because every binding declares them (java_class!, internal reference).
optimize = true also turns on R8’s optimizations, such as inlining and class merging. They are off
by default because some libraries look up their own classes in ways their published rules miss.
Turn them on once your release build passes its walkthrough with them.
An overlay is a native Android res/ directory. Keep custom values/styles.xml, color
palettes and other native resources there. Day copies them over the generated defaults;
later overlays win, and deleted files disappear on the next preparation. These settings
apply to the generated host. Existing persistent Gradle projects keep their own settings.
Preparation and build warn when an [android] table is present but a persistent host takes
precedence, including when the table explicitly sets default values such as false.
To customize the build beyond Day’s configuration, first run
day project add-target android-mdc --persist-native-project. Put your settings in the
resulting platform/android/app/build.gradle.kts. They apply after Day’s plugin, so they
take precedence:
android {
defaultConfig {
minSdk = 26
}
}
dependencies {
implementation("com.squareup.okhttp3:okhttp:4.12.0")
}
day open -t android-mdc opens the project in Android Studio. A build started there packages the
Rust library from the last day build, so run day build -t android-mdc after changing Rust code.
Manifest customization
Keep app-specific Android declarations in platform/android/AndroidManifest.xml. The starter
includes a commented, empty overlay. This file is optional and app-owned: Day never rewrites it,
and its presence does not require a persistent Gradle project. Remove it to use only Day’s defaults.
Day’s Gradle plugin adds the overlay to every app variant, including debug and release, above
the host manifest and generated contributions from pieces, permissions, and shortcuts.
Android’s standard manifest merger combines them; Android Studio also reads edits directly
without copying the file into build/.
For example, an app can explicitly replace a generated attribute and add SDK metadata:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application android:supportsRtl="false" tools:replace="android:supportsRtl">
<meta-data android:name="com.example.sdk.OPTION" android:value="true" />
</application>
</manifest>
This is an example, not a recommended default. Use your SDK’s actual metadata names. Match
dev.daybrite.day.bridge.DayActivity to customize the primary activity. Add only needed
attributes or intent filters; copying Day’s launcher and deep-link filters can duplicate them.
Conflicting attribute values require explicit tools:replace markers; tools:node="remove"
removes a matching lower-priority declaration. Invalid XML and unresolved conflicts fail the
Gradle build with the relevant manifest paths and Android’s merge diagnostics. See Android’s
manifest merge rules.
Keep identity, version, window configuration, and portable
permissions in Day.toml. Use the overlay for Android-specific declarations
those settings do not express: package visibility queries, features, services, providers, and
application metadata. Leave package and uses-sdk out: Day and Gradle supply app identity
and SDK versions.
The same overlay works with a persistent host using Day’s current Gradle plugin. It is distinct
from platform/android/app/src/main/AndroidManifest.xml, which belongs to a persistent native
project. A custom host that does not apply Day’s plugin must configure merging itself.
Caveats
- Immersive top chrome is an opt-in. A
dev.daybrite.day.EDGE_TO_EDGE<meta-data>entry in the app manifest makes the status bar transparent and floats the nav bar over full-bleed pages; nav host items marked.immersive()keep the floating scrim bar, other pages get an opaquecolorPrimarybar. Content pads itself byday::safe_area()(Layout). Without the opt-in, content renders below the bars. - Accessibility annotations are partial; see the current limits.
- Process death is a cold start unless your app persists its own state. Day does not restore a navigation stack for you.
day-piece-maphas no Android arm (it would pull in a Google Play services dependency); it renders as a placeholder.- Runtime permissions are yours to request. Day generates the manifest entries from your
[permissions]table, andday-part-permissionsperforms the request, but Android’s never-asked and permanently-denied states are indistinguishable without app-side persistence. - The emulator leg in CI tolerates flakes; the build itself gates hard.
What each piece becomes
The generated coverage matrix, checked in CI, covers every backend; this table adds links and notes.
Pieces map to both Material Components and framework android.widget classes; the table names which
one each piece uses.
| Day piece | Android class |
|---|---|
column / row / nav_stack / section | a ViewGroup subclass that lays children out at absolute frames |
label | TextView |
button | MaterialButton |
toggle | MaterialSwitch |
slider | Slider |
text_field | TextInputLayout wrapping TextInputEditText |
secure_field | the same field with a password input type |
text_area | EditText (framework) |
picker().menu() | Spinner |
picker().segmented() | a row of framework Buttons |
picker().inline() | RadioGroup |
progress(f) | LinearProgressIndicator |
spinner() | Material LoadingIndicator (M3 Expressive) |
divider | MaterialDivider |
scroll | ScrollView / HorizontalScrollView |
list | RecyclerView with a RecyclerView.Adapter (recycled cells; drag reorder via ItemTouchHelper) |
nav | MaterialToolbar over a fragment-driven FrameLayout |
nav_menu | scrolling TextView rows |
tabs | BottomNavigationView |
cover() | a full-screen FrameLayout shell |
image | ImageView |
vector | ImageView loading a VectorDrawable staged into res/drawable/ (art outside the VectorDrawable subset ships the raster instead, with a lint heads-up) |
canvas | a View subclass replaying the display list onto Canvas |
| dialogs / alerts / prompts | MaterialAlertDialogBuilder |
| file dialogs | Storage Access Framework intents |
| context menus | PopupMenu |
external activity() | ProgressBar (framework, circular) |
external combobox() | AutoCompleteTextView |
external search_field() | EditText with IME_ACTION_SEARCH |
external date_picker() | MaterialDatePicker, or inline DatePicker |
external time_picker() | MaterialTimePicker, or inline TimePicker |
external pull_refresh() | SwipeRefreshLayout |
external webview() | WebView |
external media() | VideoView |
external lottie() | LottieAnimationView (lottie-android) |
Text measurement is Android’s View.measure with an AT_MOST width probe, so TextView
does the line breaking.
Gradle dependencies come from the pieces you use: Lottie and SwipeRefreshLayout each contribute their own coordinate, and Day merges them into the app module.