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:
Flow queries. The UI never reads from the network directly.PendingActionEntity.ActionQueueManager processes pending actions sequentially, retries up to 5× with 2s polling.EmailRepository.refreshInbox() fetches from provider and upserts into Room. Called on pull-to-refresh or periodic sync.SettingsDataStore.settingsFlow (DataStore Preferences)
└─> ViewModel collects as StateFlow
└─> Composable collects as state via collectAsState()
└─> Recomposition on any change All ViewModel → Composable communication uses StateFlow. One-shot events (SentEmailEvent, ScheduledEmailEvent) use SharedFlow with replay.
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:
GmailProvider — Retrofit + Gmail REST API v1OutlookProvider — Retrofit + Microsoft Graph APIImapProvider — Eclipse Angus Mail (Jakarta Mail 2.x)A ProviderFactory lambda (injected via Hilt) resolves the correct provider for a given UserProfile.
// 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.