-
Notifications
You must be signed in to change notification settings - Fork 0
UX & UI Concepts
This project implements specific User Experience (UX) principles to ensure the app feels polished, responsive, and native.
Context: Selection screens (e.g., Default Currency).
When a user selects an item in a list that acts as a definitive action (single selection), we shouldn't force them to press "Back". However, navigating back immediately feels abrupt and leaves the user wondering if the tap "registered".
Implementation:
- User taps item.
- Immediate Feedback: The UI updates (checkbox appears) instantly.
-
Delay: We wait for
200ms(defined asUiConstants.NAV_FEEDBACK_DELAY). - Action: Navigation pops back automatically.
Context: Loading async data (e.g., Fetching Expenses or User Preferences).
The Problem: If we initialize a ViewModel state with a default value (e.g., "EUR") while loading the real value (e.g., "USD"), the user sees a "flicker": EUR -> USD in a split second. Similarly, showing a blank white screen while data loads makes the app feel slow.
The Solution:
-
ViewModel: Initialize state as
nullorLoading. -
Small UI (Text): Use an invisible placeholder (e.g.,
Modifier.alpha(0f)) to reserve space and prevent layout shifts. - Complex UI (Lists): Use Shimmer (Skeleton) Loading.
We use a standard ShimmerLoadingList component that mimics the layout of the actual content (Cards, Rows) with a pulsating gradient. This reduces perceived wait time.
Anti-Flicker: Deferred Loading Container
When responses are fast (< 150ms), showing a shimmer skeleton and immediately replacing it with content creates an ugly flicker — the "flash of loading state". We solve this with DeferredLoadingContainer:
-
Show delay (
LOADING_SHOW_DELAY_MS = 150ms): The shimmer is NOT shown immediately. If data arrives within this window, the shimmer is skipped entirely and content appears instantly. -
Minimum display time (
LOADING_MIN_DISPLAY_TIME_MS = 500ms): If the shimmer does appear (because loading took longer than the delay), it stays visible for at least 500ms so it doesn't flash and disappear.
// UI Implementation — wrap with DeferredLoadingContainer
DeferredLoadingContainer(
isLoading = uiState.isLoading,
loadingContent = { ShimmerLoadingList() }
) {
when {
uiState.errorMessage != null -> { ErrorView(...) }
uiState.items.isEmpty() -> { EmptyStateView(...) }
else -> { LazyColumn { ... } }
}
}Both timing constants live in UiConstants and can be overridden per call-site if a specific screen needs different thresholds.
Context: The Primary Floating Action Button (FAB).
Material Design 3 encourages "Expressive" motion. We avoid static, boring interactions.
Implementation:
The ExpressiveFab uses a custom Morph shape:
- Idle State: An organic 7-point "blob" or "star" shape.
- Pressed State: Smoothly morphs into a rounded "flower" shape.
-
Touch Feedback: Scales down slightly (
0.9x) on press.
These subtle animations make the app feel tactile and alive without blocking the user's task.
Context: The Main Screen and Bottom Navigation.
We strictly follow modern Android Edge-to-Edge guidelines.
-
Content: Lists scroll behind the Bottom Navigation Bar and Status Bar. We manually handle
WindowInsetsto ensure the last item is not obscured. - Glass Effect: The Bottom Bar uses a library called Haze to create a real-time blur effect (frosted glass) over the scrolling content behind it, providing depth and context.
Context: Lists with no data.
An empty white screen looks like a bug. We use a standardized EmptyStateView component when a list is valid but empty.
Guidelines:
- Icon: Large, outlined icon relevant to the feature.
- Title: Clear statement ("No expenses yet").
- Description: Helpful guidance ("Tap the + button to add one").
- Alignment: Centered vertically and horizontally.