Data Flow Guide
This guide explains how data flows through the application layers, covering different architectural patterns for network-only, local-only, and offline-first (network + local) data sources.
Table of Contents
- Architecture Overview
- Data Flow Patterns
- Network-Only Pattern
- Local-Only Pattern
- Offline-First Pattern (Network + Local)
- Real-Time Data Updates
- Caching Strategies
- Error Handling
Architecture Overview
Two-Layer Architecture
This template intentionally uses a simplified two-layer architecture (UI + Data) instead of the traditional three-layer approach. There is no domain layer by design to reduce complexity.
graph TB
subgraph UI["UI Layer (MVVM)"]
VM[ViewModel<br/>- Manages UI state UiState<ScreenData><br/>- Calls repositories directly<br/>- Transforms data for UI]
end
subgraph Data["Data Layer"]
Repo[Repository<br/>Interface + Implementation<br/>- Coordinates data sources<br/>- Implements business logic<br/>- Returns Flow<T> for reactive data<br/>- Returns Result<T> for one-shot operations]
Local[LocalDataSource<br/>Room]
Network[NetworkDataSource<br/>Retrofit]
end
UI -->|Result< T> / Flow< T>| Data
Repo --> Local
Repo --> Network
Unidirectional Data Flow
Data flows in one direction through the layers:
graph LR
A[User Interaction] -->|event| B[UI Layer]
B -->|action| C[ViewModel]
C -->|call| D[Repository]
D -->|query| E[Data Sources]
E -->|data| D
D -->|Flow/Result| C
C -->|StateFlow| B
B -->|recompose| F[UI Rendered]
Flow Steps:
- User Interaction → UI Layer
- ViewModel → Calls Repository
- Repository → Coordinates Data Sources (Room/Retrofit/Firebase/DataStore)
- Data Sources → External Systems (Database/Network/Storage)
- Data flows back → Repository → ViewModel → UI
Key Principles
- Single Source of Truth: Local database (Room) is the source of truth for observable data
- Repositories Expose Flow: Observable data uses
Flow<T>, one-shot operations useResult<T> - ViewModels Call Repositories Directly: No domain layer, repositories contain business logic
- Offline-First: Local data is displayed immediately, network updates happen in background
- Error Handling: Use
Result<T>for error propagation,suspendRunCatchingfor repository operations
Data Flow Patterns
The template supports three main data flow patterns. Choose based on your feature requirements:
| Pattern | Use Case | Data Source | Example |
|---|---|---|---|
| Network-Only | Non-cacheable data, always fresh | Retrofit API | Weather data, stock prices |
| Local-Only | User preferences, settings | Room or DataStore | Theme preference, auth token |
| Offline-First | Core app data, sync required | Room + Retrofit/Firebase | User posts, profile data |
Network-Only Pattern
When to Use
- Data must always be fresh (e.g., live sports scores, stock prices)
- Caching would provide stale or incorrect information
- Data is not critical for offline access
Architecture
graph LR
VM[ViewModel] --> Repo[Repository]
Repo --> Network[NetworkDataSource<br/>Retrofit]
Network --> API[API]
API -->|Result<T>| Network
Network --> Repo
Repo --> VM
Data Flow Diagram
sequenceDiagram
participant User
participant UI
participant VM as ViewModel
participant Repo as Repository
participant Network as NetworkDataSource
participant API
User ->> UI: Tap "Refresh"
UI ->> VM: loadWeather()
VM ->> Repo: getCurrentWeather()
Repo ->> Network: fetch()
Network ->> API: Retrofit call
API -->> Network: Weather data
Network -->> Repo: Result Weather
Repo -->> VM: Result Weather
VM ->> VM: Update UiState
VM -->> UI: StateFlow emits
UI ->> UI: Recompose with new data
Key Characteristics
- No local caching - Data fetched directly from network
- Always fresh - Every request goes to the server
- No offline support - Requires active network connection
- Returns Result
- One-shot operations for network calls
Note
For complete repository implementation examples including interface definitions, data sources, and mapper functions, see the Data Module README.
Local-Only Pattern
When to Use
- User preferences and settings
- Authentication tokens and session data
- Data that doesn't require network sync
- Small, simple key-value data
Architecture
graph LR
VM[ViewModel] --> Repo[Repository]
Repo --> DS[DataStore / Room]
DS --> Storage[(Local Storage)]
Storage -->|Flow<T>| DS
DS --> Repo
Repo --> VM
Data Flow Diagram
sequenceDiagram
participant User
participant UI
participant VM as ViewModel
participant Repo as Repository
participant DS as DataStore
Note over UI, DS: App Launch
UI ->> VM: observeSettings()
VM ->> Repo: observeSettings()
Repo ->> DS: Flow subscription
DS -->> Repo: Flow<Preferences>
Repo -->> VM: Flow emits
VM -->> UI: UiState updates
UI ->> UI: Render settings
Note over User, DS: User Changes Theme
User ->> UI: Select DARK theme
UI ->> VM: updateTheme(DARK)
VM ->> Repo: updateTheme(DARK)
Repo ->> DS: edit { ... }
DS ->> DS: Persist change
DS -->> Repo: Flow emits new prefs
Repo -->> VM: Flow emits
VM -->> UI: UiState updates
UI ->> UI: Update automatically
Key Characteristics
- Fully offline - No network dependency
- Reactive with Flow - UI automatically updates when data changes
- Immediate persistence - Changes saved to local storage instantly
- DataStore for preferences - Type-safe, reactive preferences storage
- Room for complex data - Use Room if data structure is complex
Tip
Use DataStore for simple key-value preferences and Room for structured local data with relationships.
Offline-First Pattern (Network + Local)
When to Use
- Core application data (posts, messages, user profiles)
- Data needed offline
- Data that syncs with a server
- Multi-device synchronization required
Architecture
graph TB
VM[ViewModel]
Repo[Repository<br/>Single Source of Truth]
Local[LocalDataSource<br/>Room Database]
Network[NetworkDataSource<br/>Retrofit/Firebase]
DB[(Local Database<br/>SQLite)]
API[Remote API<br/>Firestore]
VM -->|observeData| Repo
Repo -->|Flow<T>| VM
Repo -->|observes| Local
Local -->|Flow<Entity>| Repo
Repo -. sync .-> Network
Network -. fetch .-> API
Local <-->|read/write| DB
Network --> Local
Key Concepts
- Room is the Single Source of Truth: UI always observes local database
- Network Updates Background: Fetch from network, update local database
- Sync Metadata: Track sync state (lastUpdated, lastSynced, needsSync)
- Soft Deletes: Mark as deleted locally, sync deletion, then remove
Data Flow Diagram
sequenceDiagram
participant User
participant UI
participant VM as ViewModel
participant Repo as Repository
participant Local as LocalDataSource
participant Network as NetworkDataSource
participant DB as Room Database
participant API
Note over User, API: App Launch / User Navigates
UI ->> VM: observePosts()
VM ->> Repo: observePosts()
Repo ->> Repo: Trigger sync (background)
Repo ->> Local: observePosts()
Local ->> DB: Query
DB -->> Local: Flow<List<PostEntity>>
Local -->> Repo: Flow emits
Repo ->> Repo: Map to domain
Repo -->> VM: Flow<List<Post>>
VM ->> VM: Update UiState
VM -->> UI: StateFlow emits
UI ->> UI: Display posts (offline-first!)
Note over Repo, API: Background Sync
par Push Local Changes
Repo ->> Local: Get unsynced posts
Local -->> Repo: Unsynced items
Repo ->> Network: UPSERT/DELETE
Network ->> API: Push changes
Repo ->> Local: Mark as synced
and Pull Remote Changes
Repo ->> Local: Get lastSyncTimestamp
Local -->> Repo: Timestamp
Repo ->> Network: Fetch since timestamp
Network ->> API: GET updated posts
API -->> Network: Remote posts
Network -->> Repo: Remote data
Repo ->> Local: Upsert to database
Local ->> DB: Save
end
DB -->> Local: Flow emits updated
Local -->> Repo: Updated data
Repo -->> VM: Flow emits
VM -->> UI: UI updates (reactive!)
Key Characteristics
- Room as Single Source of Truth - UI always observes local database
- Immediate UI updates - Local changes reflected instantly
- Background synchronization - Network sync happens asynchronously
- Conflict resolution - Last-write-wins (server timestamp based)
- Bidirectional sync - Push local changes, pull remote changes
- Incremental sync - Only fetch data modified since last sync
Important
The offline-first pattern requires careful sync metadata tracking. Always include lastUpdated,
lastSynced, needsSync, and syncAction fields in your Room entities.
For detailed offline-first repository implementation with sync metadata, see the Data Module README.
Real-Time Data Updates
For real-time updates (e.g., Firebase Firestore snapshots, WebSocket), use Firebase's snapshot listeners or similar mechanisms.
Firebase Firestore Real-Time Flow
sequenceDiagram
participant Firestore as Firebase Firestore<br/>(Cloud)
participant Listener as Snapshot Listener
participant Flow as callbackFlow
participant Repo as Repository
participant Room as Local Database<br/>(Room)
participant VM as ViewModel
participant UI
Firestore ->> Listener: Data change
Listener ->> Flow: Emit snapshot
Flow ->> Repo: Observe changes
Repo ->> Room: Update local data
Room ->> Room: Persist
Room -->> Repo: Flow emits
Repo -->> VM: Flow<Data>
VM ->> VM: Update UiState
VM -->> UI: StateFlow emits
UI ->> UI: Render updates
Key Pattern: Firestore → Room → UI
Even with real-time updates, maintain Room as the single source of truth:
- Firestore snapshot listener emits changes
- Repository updates local Room database
- UI observes Room Flow (not Firestore directly)
- Result: Consistent data access pattern across app
This approach ensures:
- Offline access to last known data
- Consistent data access APIs
- Easy testing (mock Room, not Firestore)
- Works even if Firebase connection fails
Note
For Firebase Firestore integration examples, see the Firebase Module README.
Caching Strategies
1. Time-Based Cache Invalidation
Fetch fresh data from network if cache is older than a threshold.
Flow:
graph TD
Start[Repository called] --> Check{Check lastSyncTimestamp}
Check --> Stale{Is cache stale?<br/>current time - lastSync > threshold}
Stale -->|YES| Sync[Trigger background sync]
Stale -->|NO| Use[Use cached data]
Sync --> Emit[Emit local data immediately]
Use --> Emit
Emit --> End[offline-first!]
Use When:
- Data changes infrequently
- Staleness tolerance is acceptable (e.g., 5 minutes)
- Want to reduce network calls
2. Manual Refresh (Pull-to-Refresh)
Allow user to manually trigger sync.
Flow:
sequenceDiagram
participant User
participant UI
participant VM as ViewModel
participant Repo as Repository
participant Network
participant Room as Local Database
User ->> UI: Pull to refresh
UI ->> VM: refreshPosts()
VM ->> VM: Set loading state
VM ->> Repo: syncPosts()
Repo ->> Network: Fetch from network
Network -->> Repo: Fresh data
Repo ->> Room: Update local database
Room ->> Room: Persist
Room -->> Repo: Flow emits updated
Repo -->> VM: Updated data
VM ->> VM: Clear loading state
VM -->> UI: UiState updates
UI ->> UI: Dismiss loading indicator
Use When:
- User wants to ensure fresh data
- Complementary to time-based caching
- Provides user control
3. Network-Bound Resource Pattern
Utility for coordinating network + local data with automatic caching.
Flow:
graph TD
Start[Network-Bound Resource] --> Loading1[1. Emit Loading state]
Loading1 --> Query[2. Query local database]
Query --> Loading2[3. Emit Loading with local data]
Loading2 --> ShouldFetch{4. Should fetch<br/>from network?}
ShouldFetch -->|YES| Fetch[Fetch from network]
Fetch --> Save[Save to local database]
Save --> Success[Emit Success with fresh data]
ShouldFetch -->|NO| CachedSuccess[Emit Success with cached data]
Fetch -. Network Error .-> Error[Emit Error<br/>with stale local data]
Use When:
- Want automatic cache-then-network pattern
- Need loading states with cached data
- Want to show stale data on network errors
Note
For networkBoundResource implementation and usage examples, see
the Data Module README.
Error Handling
All data layer operations use a layered error handling approach with Result<T> for error
propagation:
- Repository Layer: Uses
suspendRunCatchingto wrap all operations - ViewModel Layer: Uses
updateStateWith/updateWithfor automatic error capture - UI Layer: Uses
StatefulComposablefor automatic error display via snackbar
Error Flow Diagram
graph TD
Start[Repository Operation] --> Catch[suspendRunCatching]
Catch --> Result{Success or Failure}
Result --> Return[Result<T>]
Return --> VM[ViewModel]
VM --> Update[updateStateWith/<br/>updateWith]
Update --> Auto[Auto-handle Result]
Auto --> UiState[UiState<br/>data or error]
UiState --> Stateful[StatefulComposable]
Stateful --> Display{Display}
Display -->|Success| Content[Show content]
Display -->|Error| Snackbar[Show error snackbar]
For comprehensive error handling patterns including network-specific errors, HTTP error codes, and detailed examples, see:
Note
Complete error handling documentation is available in the Data Module README.
Summary
This guide covered three main data flow patterns:
- Network-Only: For real-time data that doesn't need offline access (weather, stock prices)
- Local-Only: For preferences and settings using DataStore (theme, notifications)
- Offline-First: For user-generated content with Room as single source of truth (posts, profiles)
Key Takeaways:
- Choose the Right Pattern based on feature requirements
- Room is the Single Source of Truth for offline-first - UI observes local database, network updates happen in background
- Use Proper Threading - Inject
@IoDispatcherand usewithContext(ioDispatcher)for blocking calls - Error Handling is Centralized - Repository uses
suspendRunCatching, ViewModel usesupdateStateWith/updateWith, UI usesStatefulComposable - Flow for Reactive Data - Observe local database with Flow, UI updates automatically
- Result for Operations - One-shot operations return Result
for error handling
All patterns use Repositories as the interface to ViewModels, Data Sources for external system interaction, Result type for error handling, and Flow for reactive data streams.
Further Reading
- Data Module README - Repository patterns, implementations, and error handling reference
- State Management - Learn about ViewModel state patterns
- Architecture Overview - Understand the two-layer architecture
- Adding Features - Step-by-step implementation guide
- Quick Reference - Common data flow patterns cheat sheet