📬 Reply is the Material Study email app, rebuilt from the ground up in Compose Multiplatform — running on Android, iOS and Desktop (JVM) from a single multi-module Kotlin codebase. Every screen and component of the original MDC-Android Reply sample is ported 1:1 (dimensions, type scale, colour roles, shapes and motion), with the View-based widgets replaced by hand-built Compose components.
| Home | Navigation drawer | Account picker | Swipe to star |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
| Email (dark) | Compose (dark) | Search |
|---|---|---|
![]() |
![]() |
![]() |
Every transition of the Views app is reproduced with the same curves and timings
(fast_out_slow_in, 300 / 225 / 175ms, MDC's default container-transform thresholds,
ViewDragHelper's quintic settle, ItemTouchHelper's 250ms recover):
Card → detail (MaterialContainerTransform + MaterialElevationScale) |
FAB → compose (container transform, Slide return) |
Drawer + account sandwich (BottomSheetBehavior physics) |
|---|---|---|
![]() |
![]() |
![]() |
| Swipe to star (rebounding swipe, circular reveal) | Search (MaterialSharedAxis Z) |
Mailbox switch (MaterialFadeThrough) |
|---|---|---|
![]() |
![]() |
![]() |
Motion is implemented in :core:designsystem → motion/: MaterialMotion (elevation scale, fade through,
shared axis Z, slide), ContainerTransform (a port of MaterialContainerTransform's fit-mode /
fade-mode / threshold model), Interpolators (the platform interpolators), and EditReplyIcon
(the avd_edit_to_reply AnimatedVectorDrawable re-drawn on a Canvas).
- Android —
./gradlew :androidApp:assembleDebug, or run theandroidAppconfiguration in Android Studio. - Desktop —
./gradlew :desktopApp:run - iOS — open
iosApp/iosApp.xcodeprojin Xcode, set your signing TEAM_ID iniosApp/Configuration/Config.xcconfig, pick a simulator, and run. - Screenshots / motion frames —
./gradlew :desktopApp:test --tests "*ScreenshotTest*"renders every screen in both themes plus 16ms frames of each transition todesktopApp/build/screenshots/.
| Original (Views / MDC) | Compose Multiplatform |
|---|---|
Theme.Reply / Theme.Reply.Dark, TextAppearance.Reply.* (Work Sans), ShapeAppearance.Reply.* |
ReplyTheme, ReplyColors, ReplyTypography, ReplyShapes (:core:designsystem) |
BottomAppBar with FAB cradle (fabCradleMargin 8dp, fabCradleRoundedCornerRadius 32dp) + hideOnScroll |
ReplyBottomAppBar + CutoutTopEdgeShape (port of BottomAppBarTopEdgeTreatment), scroll-driven hide |
FloatingActionButton with asl_edit_reply state list, fab_show/fab_hide animators |
ReplyFab + EditReplyIcon (AVD morph), 175ms scale/opacity show-hide |
MaterialCardView whose top-left corner morphs to 24dp when starred |
ReplyCard(topLeftCorner) |
ReboundingSwipeActionCallback + EmailSwipeActionDrawable (spring swipe, circular reveal, star bounce) |
Modifier.reboundingSwipe + EmailListItem draw-behind |
BottomNavDrawerFragment: BottomSheetBehavior (hideable, skipCollapsed, half ratio 0.6), SemiCircleEdgeCutoutTreatment, account "sandwich" |
BottomNavDrawer + BottomNavDrawerState (anchored drag, nested scroll, sandwich animation) |
MenuBottomSheetDialogFragment |
MenuBottomSheet |
HomeFragment, EmailFragment (masonry attachment grid), ComposeFragment (sender spinner, avatar chips, expanding recipient card), SearchFragment |
:feature:home, :feature:email, :feature:compose, :feature:search |
MaterialContainerTransform (card → detail, FAB → compose, chip → recipient card) |
ContainerTransform overlay (:core:designsystem/motion) |
MaterialElevationScale, MaterialFadeThrough, MaterialSharedAxis(Z), Slide |
MaterialMotion enter/exit transitions on the Nav3 back stack |
avd_edit_to_reply FAB icon morph |
EditReplyIcon (Canvas-drawn AVD timeline) |
| Dark-theme menu (Light / Dark / System default) | ThemeMode in App |
- Kotlin Multiplatform, Compose Multiplatform (Material 3 primitives, custom Material 2–styled components).
- Navigation 3 — type-safe
NavKeyback stack,rememberDecoratedNavEntries(saveable state + entry-scoped ViewModels); screens are switched by anAnimatedContentthat carries the Material transitions. - Lifecycle & ViewModel —
org.jetbrains.androidx.lifecycle. - Metro — compile-time DI (
@Inject,@AssistedInject,@DependencyGraph). - Sketch — image loading (memory/disk cache, async decode, downsampling): photos and avatars are
Compose-resource URIs when the bundled data is shown and GitHub raw URLs once the remote data arrives, with the bundled
drawable as
error()fallback. - Compose Resources — Work Sans fonts, vector icons, and the bundled copies of the photos.
- Room KMP (
:core:database, bundled SQLite driver, KSP) — the local source of truth: seeded from the packaged JSON on first launch, topped up from GitHub afterwards (INSERT OR IGNORE, so stars / trash / active account survive restarts and refreshes); stores observe DAOFlows. - DataStore (
:core:datastore) — theme mode and last-sync time asFlows. - Sync status (
SyncStatusIdle/Syncing/Synced/Failed) drives a 2dp progress line, a Retry snackbar when offline, and a "Synced 2 min ago" caption in the drawer (tickingproduceStateclock). - Flows: DAO flows →
combine→stateIn(WhileSubscribed)in ViewModels; search isdebounce+distinctUntilChanged+flatMapLatestover the mail store with live results. - Ktor client + kotlinx.serialization — the sample data lives as JSON in this repo
(
core/data/src/commonMain/composeResources/files/*.json), bundled with the app for an instant offline start and refreshed from GitHub raw on launch (ReplyRepository).EmailStore/AccountStoreare in-memoryStateFlows.
See ARCHITECTURE.md for the layer map and the reasoning. In short: Room is the single source of
truth, the repository seeds it from bundled JSON and tops it up from GitHub, everything reaches the UI as Flows,
screens are stateless composables, and the shell owns navigation/transforms in small @Stable state holders.
Quality gates: detekt + Compose rules (./gradlew detekt), 23 tests (motion math, swipe/drawer physics,
repository with fake API + fixed clock, debounced search, headless screenshots) and GitHub Actions CI.
:shared # App shell: App, ReplyApp, ReplyNavigator (Nav3 back stack + Material transitions), TransformOverlay, ReplyBottomBar, Metro AppGraph
:core:datastore # Preferences DataStore (per-platform file locations)
:core:database # Room entities/DAOs/database + per-platform builders (Android / desktop file / iOS documents, in-memory for tests)
:core:data # Models, EmailStore/AccountStore over Room, ReplyRepository (bundled JSON seed + Ktor refresh), ImageResolver
:core:designsystem # ReplyTheme, palette/type/shape/motion, custom components, cutout shape, fonts/icons
:feature:home # Mailbox list, swipe-to-star, long-press menu
:feature:email # Email detail with attachment grid
:feature:compose # Compose/reply screen
:feature:search # Search suggestions
:feature:nav # Bottom navigation drawer + account picker
:androidApp / :desktopApp / iosApp
Design and assets from the Material Studies — Reply and the material-components-android-examples repository (Apache 2.0). Work Sans by Wei Huang (OFL).
Copyright 2026 AndroidPoet
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.












