com.anshtya.jetx/
├── core/
│ ├── database/ # Room database — entities, DAOs, data sources
│ ├── network/ # Retrofit APIs, WebSocket, services, DI modules
│ ├── preferences/ # DataStore and EncryptedSharedPreferences
│ ├── model/ # Domain models (Chat, UserProfile, MessageType)
│ ├── ui/ # Shared Composable components
│ └── coroutine/ # Coroutine dispatcher providers
├── auth/ # Authentication (login, register, logout)
├── profile/ # User profile creation and management
├── chats/ # Chat list and messaging
├── attachments/ # File and image handling
├── notifications/ # Notification management
├── work/ # Background workers (WorkManager)
├── fcm/ # Firebase Cloud Messaging
├── s3/ # S3 cloud storage integration
├── camera/ # Camera capture
├── calls/ # Call logs
├── send/ # Send/share functionality
├── settings/ # User settings
├── onboarding/ # Onboarding flow
├── registration/ # Registration flow
└── ui/
├── app/ # App-level navigation and AppViewModel
└── main/ # Main screen navigation
Two Retrofit instances are provided via Hilt — @Base (unauthenticated, for auth endpoints) and @Authenticated (includes AuthInterceptor to attach tokens and AuthAuthenticator to refresh on 401). Each API is wrapped in a service class using safeApiCall(), which normalises responses into a NetworkResult<T> sealed class.
WebSocketManager maintains a persistent connection, monitors network state, and reconnects automatically. Incoming frames are dispatched to WebsocketMessageProcessor on the Default dispatcher, which routes them to the repository layer.
Room database with entities for messages, chats, user profiles, attachments, and a RecentMessageEntity used to efficiently power chat list queries. LocalMessagesDataSource wraps the DAOs and runs multi-table writes (insert message + create chat if absent + upsert recent message) inside a single db.withTransaction.
JetxPreferencesStore combines three stores: tokens in EncryptedSharedPreferences, and account/user state in DataStore. AuthManager owns an AuthState StateFlow derived from the token store and drives app-level navigation.
Each feature (auth, profile, chats, messages) has an interface + implementation pair bound via Hilt. Repositories are the single source of truth — they coordinate between the network services and the local database, so the rest of the app never talks to either directly.
- Auth — calls the network service, then stores or clears the session in
AuthManager. - Profile — writes to both the remote API and local DB; exposes profile data as a
Flowfrom the DB. - Chats — reads from the local DB only; remote sync is driven by the messaging layer.
- Messages — narrow data access only: read messages from the DB, insert a message, mark it received/seen on the server. Sending/receiving orchestration lives in the domain layer (see below).
Multi-step flows that span more than one repository (or a repository plus infra like WorkManager or notifications) live in <feature>/domain/ as use cases — plain classes with a single operator fun invoke(...), constructor-injected via Hilt like anything else. They exist to keep repositories narrow (one repository = one data type = one source of truth) instead of letting one repository reach into others.
A ViewModel that only needs a single repository call still talks to the repository directly — a use case is introduced only when a ViewModel would otherwise have to coordinate several repositories/managers itself.
SendChatMessageUseCase(chats/domain/) — migrates an attachment into app storage, saves the sender's profile, inserts the message intoMessagesRepository, and schedulesMessageSendWorker. CoordinatesMessagesRepository,ChatsRepository,ProfileRepository,AttachmentRepository,AuthManager, andWorkManager.ReceiveChatMessageUseCase(chats/domain/) — fetches attachment metadata for an incoming message, saves the sender's profile, inserts the message, acknowledges receipt to the server, and posts a notification. CoordinatesMessagesRepository,ProfileRepository,AttachmentRepository, andDefaultNotificationManager.
Both are called from every entry point that can send/receive a message — ChatViewModel, MediaPreviewViewModel, and ReplyReceiver call SendChatMessageUseCase; WebsocketMessageProcessor and MessageReceiveWorker call ReceiveChatMessageUseCase — so the orchestration logic exists in exactly one place instead of being duplicated per caller.
ChatScreen
▼ user taps send
ChatViewModel
▼
SendChatMessageUseCase
├─ migrate attachment to app-internal storage
├─ insert message into DB via MessagesRepository (status = SENDING) ← UI updates immediately
└─ schedule MessageSendWorker
▼
Retrofit → server
▼ on success
update message status in DB (SENDING → RECEIVED) ← UI updates
The recipient receives the message via WebSocket, replies with a MESSAGE_UPDATE, which flips the status to SEEN.
WebSocketManager
▼ NEW_MESSAGE frame
WebsocketMessageProcessor
▼
ReceiveChatMessageUseCase
├─ fetch attachment metadata from API (if any)
├─ insert message into DB via MessagesRepository ← UI updates via Flow
├─ mark message received on server
└─ post notification
▼ MESSAGE_UPDATE frame
WebsocketMessageProcessor
└─ update message status in DB ← UI updates via Flow
ChatListViewModel
_selectedFilter (MutableStateFlow)
└─ flatMapLatest
ChatsRepository.getChats(filter) ← Flow<List<Chat>> from DB
└─ stateIn(WhileSubscribed(5000))
▼
chatList StateFlow → ChatListScreen recomposes
| Concern | Library |
|---|---|
| UI | Jetpack Compose |
| Networking | Retrofit + OkHttp |
| Real-time | OkHttp WebSocket |
| Local storage | Room + DataStore |
| Secure storage | EncryptedSharedPreferences (AES256-GCM) |
| Serialization | Kotlinx Serialization |
| DI | Hilt / Dagger |
| Concurrency | Kotlin Coroutines + Flow |
| Background work | WorkManager |
| Push notifications | Firebase Cloud Messaging |
| Image loading | Coil |
| Video playback | Media3 |
| Cloud storage | AWS S3 |