Architecture Patterns

Offline-first

MonoMail uses a strict offline-first pattern:

                  +------------------+
                  |   EmailProvider  |  (Gmail/Outlook/IMAP API)
                  +--------+---------+
                           |
                    (background sync)
                           |
+--------+     +-----------v-----------+     +-------------------+
|  Room  | <---+   EmailRepository    +---->  PendingActionQueue |
|  (DB)  |     |  (single source of   |     |  (ActionQueueMgr) |
+--------+     |   truth for UI)      |     +-------------------+
    ^          +-----------+----------+            |
    |                      |                       |
    |               (Flow queries)           (sequential retry)
    |                      |
+---+----+         +------v------+
|  ViewModel        |    View     |
| (collectAsState)  |  (Compose)  |
+---------+         +-------------+

Rules:

Reactive data flow

SettingsDataStore.settingsFlow (DataStore Preferences)
  └─> ViewModel collects as StateFlow
       └─> Composable collects as state via collectAsState()
            └─> Recomposition on any change

All ViewModelComposable communication uses StateFlow. One-shot events (SentEmailEvent, ScheduledEmailEvent) use SharedFlow with replay.

Provider pattern

The EmailProvider interface abstracts all email backends:

interface EmailProvider {
    val providerName: String
    suspend fun listThreads(folder: EmailFolder, pageToken: String?): ProviderThreadListResult
    suspend fun getThread(threadId: String): ProviderThread
    suspend fun sendEmail(...)
    suspend fun archiveThread(threadId: String)
    suspend fun toggleStar(threadId: String)
    // ... etc
}

Implementations:

A ProviderFactory lambda (injected via Hilt) resolves the correct provider for a given UserProfile.

Two-phase session restore

// Phase A — synchronous, instant, no network
val hasSession = authManager.restoreSessionQuick()  // reads encrypted DataStore
// Phase B — background, involves token refresh + push registration
scope.launch { authManager.restoreSession() }

Phase A determines the start destination. Phase B runs after the first frame.

← Setup Multi-Module →