UI Layer

Screen architecture

Each screen follows this pattern:

{Screen}Screen.kt
  ├── @Composable fun {Screen}Screen(viewModel, navCallbacks)
  │     └── Collects state: val state by viewModel.state.collectAsState()
  │     └── Renders based on sealed class state
  │
  └── @HiltViewModel
      class {Screen}ViewModel @Inject constructor(
          private val repository: EmailRepository,
          private val settingsDataStore: SettingsDataStore,
          savedStateHandle: SavedStateHandle
      ) : ViewModel()

State pattern

ViewModels expose a single StateFlow of a sealed class:

sealed class InboxState {
    data object Loading : InboxState()
    data class Success(
        val threads: List<EmailThread>,
        val currentTab: InboxTab,
        val isRefreshing: Boolean,
        val isLoadingMore: Boolean,
        val nextPageToken: String?
    ) : InboxState()
    data class Error(val message: String) : InboxState()
}

ViewModel patterns

Complex state derivation (InboxViewModel)

// Combine multiple reactive sources
val state: StateFlow<InboxState> = combine(
    currentTab,
    activeAccountId,
    unifiedInboxEnabled,
    organizeByThread
) { tab, accountId, unified, threaded ->
    // Derive query parameters, return InboxState
}.flatMapLatest { params ->
    // Switch between different Room queries based on params
    when (params.tab) {
        InboxTab.INBOX -> repository.getInboxThreadsFlow(params.accountId)
        // ...
    }
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), InboxState.Loading)

One-shot events

private val _events = MutableSharedFlow<InboxEvent>()
val events: SharedFlow<InboxEvent> = _events.asSharedFlow()

Composable patterns

Theme tokens

Monochrome palette:
  Light:  Background #FFFFFF, Surface #FAFAFA, SurfaceContainer #F3F3F3
  Dark:   Background #000000, Surface #0D0D0D, SurfaceContainer #141414
  Primary: Black (light) / White (dark)
  OnSurfaceMuted: #5C5C5C (light) / #A0A0A0 (dark)

Shapes:
  extraSmall: 8dp, small: 12dp, medium: 16dp, large: 24dp, extraLarge: 32dp

Typography:
  Google Sans Rounded (W500 display, W600 headlines/titles, W400 body, W500 labels)
← Data Layer Dependency Injection →