Skip to content
This repository was archived by the owner on Jun 3, 2026. It is now read-only.

Design System

Aarsh edited this page May 25, 2026 · 2 revisions

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.onCreate calls DynamicColors.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 stack

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).


Typography

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

Colour system

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.


Shape system

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.


Buttons

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).


Chips

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/.


Motion

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.


Haptics

PreferenceHelper.performHaptics(...) / performClickHaptic(...) / performSuccessHaptic(...) are the standard call-sites. Three patterns:

  • Click — VIRTUAL_KEY / KEYBOARD_TAP for plain taps
  • Success — CONFIRM / LONG_PRESS for committing actions (save, mark paid, sign in)
  • Tick — CLOCK_TICK / SEGMENT_TICK for 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.


Icons

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

Edge-to-edge & insets

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 AppBarLayout via fitsSystemWindows="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.


What we deliberately don't do

  • No Compose (view-XML for now).
  • No custom drawables for status pills — pure MaterialCardView with cardBackgroundColor + rounded shape covers it.
  • No animated vector drawables for tap responses — relying on standard ripple + brief scale/alpha animations from AnimationHelper.

See also

Clone this wiki locally