Android (Material Components)
android-mdc renders Material Components
widgets over JNI, inside a checked-in Gradle project. The app theme is
Theme.Material3Expressive.DayNight.NoActionBar, so light and dark follow the system.
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.
cargo install day-cli cargo-ndk
day new app my-app --toolkit android-mdc
cd my-app
day launch -p android-mdc # installs on every connected device/emulator, right ABI each
Android Studio works as well — the Gradle scaffold calls back (day gradle-backend build) so a
build started from the IDE rebuilds the Rust .so the same way.
day pack -p android-mdc # release-signed .apk + .aab
Caveats and special considerations
- 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; selector items marked.immersive()keep the floating scrim bar, other pages get an opaquecolorPrimarybar. Content pads itself byday::safe_area()(Layout). Without the opt-in, everything renders below the bars as before. - 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.
- No map piece.
day-piece-maphas no Android arm (it would pull in a Google Play services dependency); it renders as a placeholder. - The list is a
ListView, not aRecyclerView. It recycles rows, but it is the framework’s older collection widget; very large or heterogeneous lists may want a native tweak. - 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
Both Material Components and framework android.widget classes are in play, and which one you get
matters — the table is explicit about it.
| Day piece | Android class |
|---|---|
column / row / 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 |
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 | ListView + a BaseAdapter |
nav | MaterialToolbar over a fragment-driven FrameLayout |
nav_menu | scrolling TextView rows |
tabs | BottomNavigationView |
cover() | a full-screen FrameLayout shell |
image | ImageView |
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 own View.measure with an AT_MOST width probe, so TextView
does the actual 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. You never edit Gradle per piece.



