Repository navigation
Design System
How the app looks the way it does — Material 3 Expressive with Material You Dynamic Colors as the canonical palette source (since v3.8-beta.9).
VarisankyaApplication.onCreatecallsDynamicColors.applyToActivitiesIfAvailable(this). On Android 12+, every M3 token (colorPrimary,colorOnPrimary,colorPrimaryContainer,colorSurface*, etc.) is filled in at runtime from the user's wallpaper-derived palette. On older devices, the static fallbacks from the theme apply automatically — same code path, just no Material-You override.Historical note: the app previously disabled Dynamic Colors in favour of a "brand monochrome" identity. That decision was reversed in v3.8-beta.9 — Dynamic Colors is the Android-native standard and gave us the right contrast for the FAB and pills without bespoke colour engineering.
| Theme | Parent | Purpose |
|---|---|---|
Theme.App.Starting |
Theme.SplashScreen |
Boot splash; postSplashScreenTheme = Theme.Varisankya
|
Theme.Varisankya |
Theme.Material3.DayNight.NoActionBar |
The actual app theme; Google Sans Flex, custom shapes, custom button/chip styles |
Theme.Varisankya.SystemFont |
Theme.Varisankya |
Same theme but resets the font family back to system default — applied when the user picks "System" in Settings → Typography |
Both apply DayNight, so light/dark switching is automatic from the system preference (or forced via AppCompatDelegate.setDefaultNightMode(...) from SettingsActivity).
Custom TextAppearance.App.* styles in res/values/type.xml. They extend the Material 3 defaults and force Google Sans Flex as the font family. A few add bold or letterSpacing tweaks (TextAppearance.App.Button, TitleLarge, DisplayMedium).
The theme overrides every textAppearance* attribute to point at the App.* variants, so any view using ?attr/textAppearanceXxx automatically gets Google Sans. SystemFont variant resets to the Material defaults (Roboto / system).
| Token | Use it for |
|---|---|
textAppearanceDisplayLarge |
Hero amounts (Home, Payment History) |
textAppearanceDisplayMedium |
Was the hero — now bumped to Display Large in v3.8-beta.7 |
textAppearanceHeadlineSmall |
Bottom sheet titles ("Manage Payments", "New Subscription") |
textAppearanceTitleLarge |
Subscription name + amount on home list; section heroes |
textAppearanceTitleMedium |
Settings card section labels |
textAppearanceTitleSmall |
Sub-section labels within cards |
textAppearanceBodyLarge |
Body copy in cards |
textAppearanceBodyMedium |
Secondary descriptions / muted text |
textAppearanceLabelLarge |
Button labels, pill text |
The app uses Material 3's standard surface roles (colorSurface, colorSurfaceContainer, colorSurfaceContainerHigh, etc.) and semantic roles (colorPrimary / colorOnPrimary, colorError / colorErrorContainer, etc.).
Hot-path resolution lives in util/ThemeHelper — adapters call ThemeHelper.getPrimaryColor(context) etc. to avoid per-bind MaterialColors.getColor overhead during list scrolling.
Semantic colour usage in the subscription list (v3.8-beta.7+):
| State | Pill background | Pill text |
|---|---|---|
| Overdue | colorErrorContainer |
colorOnErrorContainer |
| Active (any future date) | colorPrimary |
colorOnPrimary |
| Inactive (paused) | colorSurfaceVariant |
colorOnSurfaceVariant |
The Material 3 OnXxx tokens are the only safe text colours to put on top of their matching Xxx roles. Mixing (e.g. colorOnSurface on a colorPrimary background) gives broken contrast — the FAB-text bug fixed in v3.8-beta.8 was exactly this.
Defined in res/values/themes.xml and res/values/dimens.xml.
| Dimension | Value | Use |
|---|---|---|
card_corner_radius_large |
28dp | Hero cards, bottom sheet content cards, single-item or first/last items in grouped lists |
card_corner_radius_medium |
24dp | Filled buttons, the logout card |
card_corner_radius_small |
12dp | Middle items in grouped lists, inner corners on first/last |
cardCornerRadius on ExtendedFAB
|
inherits Material 3 default | 16dp by spec |
| Pill (chip / status pill) | 100dp |
Force-rounded — appears fully-circular at any height |
The grouped-list shape pattern (visually merging N cards into one continuous surface) is implemented by SubscriptionAdapter:
-
single(only item): all 4 corners large -
first: top corners large, bottom corners small -
middle: all 4 corners small -
last: bottom corners large, top corners small
Pre-built ShapeAppearanceModels in themes.xml (ShapeAppearance.App.FirstItem etc.) are cached as static fields in the adapter — avoids per-scroll allocation.
| Style | Use for |
|---|---|
Widget.App.Button |
Primary filled button. "Save Subscription", "Mark as Paid", "Confirm" |
Widget.App.Button.Tonal |
Secondary action. "Cancel", "Add Payment, Keep Due Date" |
Widget.App.Button.Outlined |
Tertiary / non-destructive secondary |
App.Button.Destructive |
Outlined error tone. Currently unused in layouts but available for delete actions |
App.Button.Destructive.Icon |
Circular outlined error icon button. "Delete" in Edit Subscription sheet |
All variants share ShapeAppearance.App.Button (28dp corners, fully-pill at typical heights) and TextAppearance.App.Button (Google Sans bold + 0.05 letter spacing).
Single project-wide style: Widget.App.Chip (extends Widget.Material3.Chip.Filter).
- Unselected: rounded shape (100dp pill), light background, dark text, 0.8dp stroke.
- Selected: dark background, light text, slightly squarer shape (12dp corners).
The "swapped" treatment (dark = selected) is deliberate — at-a-glance the selected chip contrasts against unselected ones. Selection state is also reflected via the chipBackgroundColor / chipStrokeColor ColorStateList resources in res/color/.
Imports com.google.android.material.transition.platform.MaterialSharedAxis and uses MaterialSharedAxis.Z for activity enter/return transitions (depth in/out — feels like content "coming forward").
Constants in Constants.kt:
| Constant | Value | M3 token |
|---|---|---|
ANIM_DURATION_SHORT |
100ms | Short |
ANIM_DURATION_MEDIUM |
300ms | Medium |
ANIM_DURATION_LONG |
500ms | Long |
Interpolator: FastOutSlowInInterpolator (Material's standard "decelerate then settle" curve).
Bottom sheet drag handles scale up to 1.2× on STATE_EXPANDED, shrink to 0.9× on STATE_DRAGGING, settle back to 1.0× on STATE_SETTLING / STATE_COLLAPSED. With haptic ticks on each state change. Adds tactile "weight" to the gesture.
PreferenceHelper.performHaptics(...) / performClickHaptic(...) / performSuccessHaptic(...) are the standard call-sites. Three patterns:
-
Click —
VIRTUAL_KEY/KEYBOARD_TAPfor plain taps -
Success —
CONFIRM/LONG_PRESSfor committing actions (save, mark paid, sign in) -
Tick —
CLOCK_TICK/SEGMENT_TICKfor scroll milestones, drag-handle interactions, chip selections
All haptics respect Settings → Haptics → Off, which sets a PreferenceHelper.isHapticsEnabled(context) flag. When off, every performHaptics call early-returns silently.
Vector drawables under res/drawable/ic_*.xml. The current set is intentionally minimal:
ic_add, ic_back, ic_check, ic_check_circle, ic_chevron_right, ic_close, ic_delete, ic_delete_sweep, ic_edit, ic_history, ic_list, ic_open_in_new, ic_pause, ic_power, ic_search, ic_settings, ic_show_chart, ic_autopay, ic_google_logo, ic_logo_mono, plus launcher / splash assets.
If you need a new icon, prefer Google's Material Symbols (export as SVG → drop into res/drawable after stripping fixed colours). The launcher / playstore icons live separately:
-
res/mipmap-*(per density) — launcher -
app/src/main/ic_launcher-playstore.png(512×512) — Play Console listing
targetSdk = 36 makes Android 15+ apply edge-to-edge automatically. MainActivity adds explicit WindowInsetsCompat handling:
- Bottom inset → padding on
NestedScrollView+ offset on the Extended FAB so it floats above the nav bar. - Top inset → handled by
AppBarLayoutviafitsSystemWindows="true".
The status bar is transparent (@android:color/transparent) with windowLightStatusBar=true so icons stay dark on light backgrounds. Dark mode flips this automatically.
Predictive back gesture is enabled (android:enableOnBackInvokedCallback="true" in the manifest) — the system shows a preview of the underlying screen while the back gesture is in flight.
- No Compose (view-XML for now).
-
No custom drawables for status pills — pure
MaterialCardViewwithcardBackgroundColor+ rounded shape covers it. -
No animated vector drawables for tap responses — relying on standard ripple + brief scale/alpha animations from
AnimationHelper.
- Architecture — where the design system gets applied.
- Known Issues — outstanding rough edges in the design layer.
Source: aarshps/varisankya-android · This wiki is human-facing; the repo itself targets AI coding agents (AGENTS.md).