Skip to content

NhuHuy-79/Replee

Repository files navigation

Replee

πŸ”¨ Modern Realtime 1v1 Chat: Jetpack Compose + Firebase + Room. Offline-first architecture with Material 3. πŸš€πŸ’¨

🧰 Tech Stack & Open Source Libraries

πŸ“± Core Configuration

  • Minimum SDK: 30 (Android 11)
  • Language: Kotlin
    • Utilizes Coroutines & Flow for reactive and asynchronous programming

🧩 Jetpack Components

  • Jetpack Compose – Modern toolkit for building native UI
  • Navigation (v3) – Type-safe and flexible screen navigation
  • Hilt – Dependency Injection (DI)
  • Room Database – Local persistence for offline support
  • Paging 3 – Efficient large dataset loading
  • ViewModel – Lifecycle-aware state management
  • WorkManager – Background task scheduling
  • DataStore – Modern key-value storage for preferences

☁️ Cloud Services & Backend

Tip

You can checkout Backend For Replee here.

  • Cloudinary: Media upload, storage, and transformation

🌐 Networking & Media

  • Retrofit & Gson – HTTP client & JSON parsing
  • Coil – Image loading and caching
  • Zoomable – Pinch-to-zoom support for images

🎨 UI/UX & Utilities

  • MaterialKolor – Dynamic Material 3 color generation
  • Timber – Logging utility
  • unDraw – Customizable illustrations
  • Flow Operator – Simplifies complex Flow transformations

πŸ§ͺ Testing

  • Truth – Readable assertions
  • MockK – Kotlin-first mocking library
  • Coroutines Test – Testing suspend functions
  • Turbine – Testing Kotlin Flow emissions

Project Architechture

Replee is built upon the MVI (Model-View-Intent) architectural pattern, integrated with Clean Architecture principles. This ensures a predictable state management system, high testability, and a clear separation of concerns.

πŸ“ Architectural Pattern: MVI (Unidirectional Data Flow)

Unlike traditional MVVM, Replee leverages MVI to handle complex UI states in a chat environment. The data flows in a single direction, making the app easier to debug and scale.

  • Model (State): A single, immutable source of truth for the UI state. Any change in the data results in a new State being emitted to the View.

  • View: Jetpack Compose functions that observe the State and render the UI. The View doesn't hold logic; it only displays what the State dictates.

  • Intent (Actions): Represents the user's intention (e.g., SendMessage, LoadChatHistory). These intents are dispatched to the ViewModel to trigger business logic.

Modularization

Replee is built with a multi-module architecture to ensure a highly scalable and maintainable codebase. By separating features and core logic into independent modules, we achieve faster build times, better separation of concerns, and improved reusability across the project.

project-module:
  - app: App initialization, Dependency Injection (Hilt) configuration, global navigation hosting, notifications, services,etc
  - core/
    - domain: Contains Business Logic, Entities, and Repository Interfaces. No Android dependencies
    - data: The implementation of repositories. Orchestrates data from Network and Database.
    - network: Infrastructure for API calls (Retrofit, Firebase, Cloudinary).
    - database: Local persistence using Room Database.
    - design_system: Shared UI components, Theme, and Design Tokens (Material 3).
    - common: Shared utilities, extensions, and base classes used by everyone.
    - test: Shared testing frameworks and test doubles (MockK , Turbine).

  - feature_autht: Supports Multi-method authentication including Email/Password and Google One Tap Sign-In.
  - feature_chat: : Real-time messaging with Instant Delivery Status, image sharing (integrated with Cloudinary).
  - feature_profile: Custom profile customization including Avatar uploads.

  Note: All feature contain UI logic (Compose), ViewModels, and MVI State management.
  They are isolated from each other. They only communicate through the :app module or shared :core interfaces.

Offline-first Strategy

Replee is designed to be fully functional in low-connectivity environments, ensuring a seamless user experience regardless of network status.

πŸ”„ Data Synchronization Flow

We implement the Single Source of Truth pattern using Room as the local cache and Firestore as the remote source.

  • Local-Persistence: All data is first persisted in the local Room database before being displayed.

  • Background Sync: Using WorkManager and Coroutines, the app synchronizes local changes with the backend once the connection is restored.

  • Real-time Listening: We use Firestore Snapshots to listen for remote changes and immediately update the local cache.

Code sample
  sealed class DataChange<out T> {
    data object Empty : DataChange<Nothing>()
    data class Upsert<out T>(val data: T) : DataChange<T>()
    data class Delete(val id: String) : DataChange<Nothing>()
  }

  //Listen Data From Network
  class ListenMessageChangeUseCase @Inject constructor(
    private val messageRepository: MessageRepository
  ) {
    operator fun invoke(conversationId: String): Flow<List<DataChange<Message>>> {
      return messageRepository.observeNetworkMessageChange(conversationId)
    }
  }

  //Update data changes from Network to Local
  class UpdateMessageChangeUseCase @Inject constructor(
    private val messageRepository: MessageRepository
  ) {
      suspend operator fun invoke(
          dataChanges: List<DataChange<Message>>
      ) {
          val upserts: MutableList<Message> = mutableListOf()
          val deletes: MutableList<String> = mutableListOf()

          for (change in dataChanges) {
              when (change) {
                  is DataChange.Delete -> deletes.add(change.id)
                  is DataChange.Upsert -> upserts.add(change.data)
                  DataChange.Empty -> {
                      //TODO
                  }
              }
          }

          Timber.d("Upserts: $upserts")
          Timber.d("Deletes: $deletes")

          messageRepository.updateLocalDataChange(
              upsert = upserts,
              delete = deletes
          )
      }
  }

  //Execute in ViewModel
   private fun listenToMessageChange() {
      viewModelScope.launch {
          listenMessageChangeUseCase(conversationId = conversationId)
              .collect { dataChanges ->
                  updateMessageChangeUseCase(dataChanges)
              }
      }
    }

⚠️ Error Handling & Resilience

Our strategy focuses on:

  • Graceful Degradation: Showing cached data when the network fails.

  • Retry Policies: Exponential backoff for failed network requests.

  • Visual Feedback: Clear indicators for "Pending", "Synced", or "Failed" states.

Code sample
  class ReadMessageUseCase @Inject constructor(
    private val syncManager: SyncManager,
    private val conversationRepository: ConversationRepository,
    private val messageRepository: MessageRepository,
    private val workerScheduler: WorkerScheduler
) {
    suspend operator fun invoke(
        conversationId: String,
        receiverId: String,
    ): NetworkResult<String> {
        return conversationRepository.markAllMessagesRead(
            conversationId = conversationId,
            currentUserId = receiverId
        ).then { conversationId ->
            messageRepository.markAllMessagesRead(
                conversationId = conversationId,
                receiverId = receiverId
            )
        }
            //Mark unsynced in Local Database
            .onFailure {
                syncManager.updateConversationStatus(
                    conversationId = conversationId,
                    synced = false
                )
                //Schedule a synchronizing worker
                workerScheduler.scheduleConversationSyncWorker()
            }
            //Mark synced in LocalDatabase
            .onSuccess {
                syncManager.updateConversationStatus(conversationId = conversationId, synced = true)
            }
    }
}

About

πŸ”¨ Modern Realtime 1v1 Chat: Jetpack Compose + Firebase + Room. Offline-first architecture with Material 3. πŸš€πŸ’¨

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages