Android (Material Components)

Tier 1 · Supported

android-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 opaque colorPrimary bar. Content pads itself by day::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-map has 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, and day-part-permissions performs 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 pieceAndroid class
column / row / nav_stack / sectiona ViewGroup subclass that lays children out at absolute frames
labelTextView
buttonMaterialButton
toggleMaterialSwitch
sliderSlider
text_fieldTextInputLayout wrapping TextInputEditText
secure_fieldthe same field with a password input type
text_areaEditText (framework)
picker().menu()Spinner
picker().segmented()a row of framework Buttons
picker().inline()RadioGroup
progress(f)LinearProgressIndicator
spinner()Material LoadingIndicator (M3 Expressive)
dividerMaterialDivider
scrollScrollView / HorizontalScrollView
listRecyclerView with a RecyclerView.Adapter (recycled cells; drag reorder via ItemTouchHelper)
navMaterialToolbar over a fragment-driven FrameLayout
nav_menuscrolling TextView rows
tabsBottomNavigationView
cover()a full-screen FrameLayout shell
imageImageView
vectorImageView loading a VectorDrawable staged into res/drawable/ (art outside the VectorDrawable subset ships the raster instead, with a lint heads-up)
canvasa View subclass replaying the display list onto Canvas
dialogs / alerts / promptsMaterialAlertDialogBuilder
file dialogsStorage Access Framework intents
context menusPopupMenu
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.