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 opaque colorPrimary bar. Content pads itself by day::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-map has no Android arm (it would pull in a Google Play services dependency); it renders as a placeholder.
  • The list is a ListView, not a RecyclerView. 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, 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

Both Material Components and framework android.widget classes are in play, and which one you get matters — the table is explicit about it.

Day pieceAndroid class
column / row / stack / sectiona ViewGroup subclass that lays children out at absolute frames
labelTextView
buttonMaterialButton
toggleMaterialSwitch
sliderSlider
text_fieldTextInputLayout wrapping TextInputEditText
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
listListView + a BaseAdapter
navMaterialToolbar over a fragment-driven FrameLayout
nav_menuscrolling TextView rows
tabsBottomNavigationView
cover()a full-screen FrameLayout shell
imageImageView
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 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.