Android Cross-Page Sync Without an Event Bus: A Full Worked Demo
Event buses and global mutable state remain common in Android codebases, but they create implicit coupling that breaks testability and makes state restoration fragile. This demo shows a complete, Hilt-wired alternative using only standard library components — `Flow`, `DataStore`, `ViewModel`, and Compose — that scales from a single module to a multi-module project.
Cross-page coordination in Android — login state, message badges, theme switching, global snackbars — is reframed as a state-management problem, not a notification problem. A full demo walks through defining sealed business models, persisting state with DataStore, exposing continuous flows from Repository singletons, and binding interfaces with Hilt. ViewModels combine raw business streams into derived UI state using `combine` and `stateIn`, while Compose screens observe only what they need via `collectAsStateWithLifecycle`.
One-time side effects like snackbars move to `SharedFlow` and are collected once at the top level. The login dialog closes declaratively by watching `loginState`, and theme changes propagate from DataStore through a Repository to the Activity without any manual dispatch. The demo then pushes dependent state logic down into the Repository itself, using `flatMapLatest` to switch message streams when the user logs in or out, so ViewModels never touch tokens.
Componentized projects slot into the same pattern by placing Repository interfaces in a base module and letting Hilt wire implementations at the app level. The result is a system where pages never call each other, ViewModels never reference each other, and all synchronization flows from a single source of truth.
Framing cross-page sync as a state-derivation problem eliminates an entire category of lifecycle bugs and ordering dependencies that event buses introduce.
The `combine` operator in ViewModels acts as a declarative rule engine — "show badge = logged in AND count > 0" — that is testable without any UI or navigation framework.
Using `WhileSubscribed(5000)` on `stateIn` keeps upstream flows active for a grace period during configuration changes, avoiding restart costs while still cleaning up when the consumer truly disappears.
Pushing dependent state logic into the Repository via `flatMapLatest` is underused; it removes token-awareness from ViewModels and makes the data layer self-consistent when users switch.
The distinction between `StateFlow` for persistent state and `SharedFlow` for one-shot signals is simple but frequently violated in practice, leading to missed events or stale UI.
DataStore paired with Repository flows gives global settings like theme a single source of truth that survives process death without any manual save/restore code.
Componentized architecture works naturally with this pattern because Repository interfaces act as the API surface between modules — no shared ViewModels or event objects needed.
The demo's refusal to let ViewModels reference each other is the architectural linchpin; it forces every piece of shared state to have an explicit, testable home.