diff --git a/README.md b/README.md
index 2add6bc8d..c057d6f67 100644
--- a/README.md
+++ b/README.md
@@ -7,153 +7,629 @@
-## What is it
+# Jetpack Compose Starter 🚀
-It's a starting template that I use for all my Android apps. It is based on the architecture of the [Now In Android](https://github.com/android/nowinandroid) app by Google. Check out the app from the latest [Release](https://github.com/atick-faisal/Jetpack-Compose-Starter/releases).
+A robust, production-ready template for modern Android development that takes the pain out of setting up a new project. Built on the foundation of [Now In Android](https://github.com/android/nowinandroid)'s architecture, this template provides a comprehensive starting point for both new and experienced Android developers.
-![Screenshots](https://github.com/atick-faisal/Jetpack-Compose-Starter/blob/main/assets/ss.png)
+## Why This Template?
-> [!WARNING]
-> Firebase authentication and crashlytics requires Firebase console setup and the `google-services.json` file. I have provided a template to ensure a successful build. However, you need to provide your own in order to use all the functionalities.
+- **Production-Ready Authentication**: Firebase authentication with Google Sign-In and email/password, including secure credential management
+- **Clean Architecture**: Clear separation of concerns with a modular, scalable architecture
+- **Modern Tech Stack**: Leverages the latest Android development tools and libraries including Jetpack Compose, Kotlin Coroutines, and Dagger Hilt
+- **Type-Safe Navigation**: Fully typed navigation using Kotlin serialization
+- **Robust Data Management**: Complete data layer with Repository pattern, Room database, and Preferences DataStore
+- **Network Communication**: Retrofit + OkHttp setup with proper error handling and interceptors
+- **CI/CD**: Automate build, release and Play Store deployment using GitHub actions and Fastlane
-## Documentation
+> [!NOTE]
+> The codebase follows a set of conventions that prioritize simplicity and maintainability. Understanding these patterns will help you develop effectively.
-
-
-
-
- Read The Documentation Here
-
+## Technical Stack
+
+### Core Libraries
+- **UI**: Jetpack Compose, Material3, Navigation Compose
+- **DI**: Dagger Hilt
+- **Async**: Kotlin Coroutines & Flow
+- **Network**: Retrofit, OkHttp, Kotlinx Serialization
+- **Storage**: Room DB, DataStore Preferences
+- **Images**: Coil
+
+### Build & Tools
+- Kotlin 2.0
+- Gradle 8.11.1 with Version Catalogs
+- Java 21
+- Custom Gradle Convention Plugins
+- Spotless for code formatting
+
+### Architecture Components
+- MVVM with Clean Architecture
+- Repository Pattern
+- Modular design with feature isolation
+- Firebase Authentication
+- Single Activity
+- DataStore for preferences
+- Kotlinx Serialization for JSON
+- Type-safe navigation
+
+### Development Features
+- Debug/Release variants
+- Firebase Crashlytics integration
+- GitHub Actions CI/CD
+- Automatic dependency updates with Renovate
+- Code documentation with Dokka
+
+## Project Architecture
+
+```mermaid
+graph TD
+ A[App Module] --> B[Auth]
+ A --> C[Network]
+ A --> D[Storage:Room]
+ A --> E[Storage:Preferences]
+ B --> F[Core:UI]
+ B --> E
+ B --> G[Core:Android]
+ C --> G
+ D --> G
+ E --> G
+ F --> G
+```
+
+## Architecture Layers
-## Features
+The codebase follows a clean architecture pattern with clear separation of concerns across different layers. Each layer has specific responsibilities and dependencies flow inward, with the domain layer at the center.
-This template offers Modern Android Development principles and Architecture guidelines. It provides an out-of-the-box template for:
+### Data Layer
-- Connecting to a remote API using Retrofit and OKHttp
-- Persistent database solution using Room and Datastore
-- Sign In Authentication using Firebase i.e. Google ID and Email
-- Bluetooth communication using classic and low-energy (upcoming) protocols
+The data layer is responsible for handling data operations and is organized into the following components:
+
+- **Data Sources**: Located in `*DataSource` classes (e.g., `NetworkDataSource`, `AuthDataSource`)
+ - Handle raw data operations with external systems (API, database, etc.)
+ - Perform data transformations and mapping
+ - Example: `AuthDataSourceImpl` in the auth module handles raw Firebase authentication operations
> [!NOTE]
-> Firebase auth needs [setting up](https://developers.google.com/android/guides/client-auth?sjid=11391664450238405928-EU) first using the SHA fingerprint. Get the SHA fingerprint of the app and add it to firebase console.
+> Data sources should expose Flow for observable data and suspend functions for one-shot operations:
+> ```kotlin
+> interface DataSource {
+> fun observeData(): Flow
+> suspend fun updateData(data: Data)
+> }
+> ```
+
+- **Models**: Found in `models` packages across modules
+ - Define data structures for external data sources
+ - Contain serialization/deserialization logic
+ - Example: `NetworkPost` in the network module represents raw API responses
+
+> [!IMPORTANT]
+> Always keep data models immutable and use data classes:
+> ```kotlin
+> data class NetworkResponse(
+> val id: Int,
+> val data: String
+> )
+> ```
+
+The data layer is implemented across several modules:
+- `network/`: Handles remote API communication
+- `storage/preferences/`: Manages local data persistence using DataStore
+- `storage/room/`: Handles SQLite database operations using Room
-It contains easy-to-use Interfaces for common tasks. For example, the following provides utilities for Bluetooth communication:
+> [!WARNING]
+> Don't expose data source interfaces directly to ViewModels. Always go through repositories:
+> ```kotlin
+> // DO THIS
+> class MyViewModel(
+> private val repository: MyRepository
+> )
+>
+> // DON'T DO THIS
+> class MyViewModel(
+> private val dataSource: MyDataSource
+> )
+> ```
+
+### Repository Layer
+
+The repository layer acts as a single source of truth and mediates between data sources:
+
+- **Repositories**: Found in `repository` packages (e.g., `AuthRepository`)
+ - Coordinate between multiple data sources
+ - Implement business logic for data operations
+ - Abstract data sources from the UI layer
+ - Handle caching strategies
+ - Example: `AuthRepositoryImpl` coordinates between Firebase Auth and local preferences
+
+Key characteristics:
+- Uses Kotlin Result type for error handling
+- Implements caching where appropriate
+- Exposes Kotlin Flow for reactive data updates
+
+> [!IMPORTANT]
+> Always return `Result` from repository methods. This ensures consistent error handling across the app:
+> ```kotlin
+> suspend fun getData(): Result = suspendRunCatching {
+> dataSource.getData()
+> }
+> ```
+
+### UI Layer
+
+The UI layer follows an MVVM pattern and consists of:
+
+- **ViewModels**: Located in `ui` packages
+ - Manage UI state and business logic
+ - Handle user interactions
+ - Communicate with repositories
+ - Example: `AuthViewModel` manages authentication state and user actions
+
+- **Screens**: Found in `ui` packages alongside their ViewModels
+ - Compose UI components
+ - Handle UI layouts and styling
+ - Observe ViewModel state
+ - Example: `SignInScreen` displays login form and handles user input
+
+- **State Management**:
+ - Uses `UiState` data class for managing loading, error, and success states
+ - Employs `StateFlow` for reactive UI updates
+ - Handles one-time events using `OneTimeEvent`
+
+
+> [!TIP]
+> Always use `UiState` wrapper for ViewModel states. This ensures consistent error and loading handling across the app.
+> ```kotlin
+> data class UiState(
+> val data: T,
+> val loading: Boolean = false,
+> val error: OneTimeEvent = OneTimeEvent(null)
+> )
+> ```
+
+> [!WARNING]
+> Don't create custom loading or error handling in individual screens. Use StatefulComposable instead:
+> ```kotlin
+> // DON'T DO THIS
+> if (isLoading) {
+> CircularProgressIndicator()
+> }
+>
+> // DO THIS
+> StatefulComposable(state = uiState) { data ->
+> // Your UI content
+> }
+> ```
+
+### Data Flow
+
+The typical data flow follows this pattern:
+
+1. **UI Layer**:
+ ```
+ User Action → ViewModel → Repository
+ ```
+
+2. **Repository Layer**:
+ ```
+ Repository → Data Sources → External Systems
+ ```
+
+3. **Data Flow Back**:
+ ```
+ External Systems → Data Sources → Repository → ViewModel → UI
+ ```
+
+### State Management and Data Structures
+
+The codebase uses several key data structures for state management:
+
+1. **UiState**:
+ ```kotlin
+ data class UiState(
+ val data: T,
+ val loading: Boolean = false,
+ val error: OneTimeEvent = OneTimeEvent(null)
+ )
+ ```
+ - Wraps UI data with loading and error states
+ - Used by ViewModels to communicate state to UI
+
+2. **Result**:
+ - Used by repositories to handle success/failure
+ - Propagates errors up the stack
+ - Example: `Result` for authentication operations
+
+3. **StateFlow**:
+ - Used for reactive state management
+ - Provides hot, stateful event streams
+ - Example: `_authUiState: MutableStateFlow>`
+
+4. **OneTimeEvent**:
+ - Errors propagate up through Result
+ - They get converted to OneTimeEvent when reaching the UI layer
+ - This ensures error Snackbars only show once and don't reappear on recomposition
+
+> [!IMPORTANT]
+> Use `StatefulComposable` for screens that need loading or error handling. This component handles these states automatically, reducing boilerplate and ensuring consistent behavior.
+> ```kotlin
+> StatefulComposable(
+> state = viewModel.state,
+> onShowSnackbar = { msg, action -> /* ... */ }
+> ) { data ->
+> // Your UI content here
+> }
+> ```
+
+> [!TIP]
+> Use the provided extension functions for updating state:
+> ```kotlin
+> // For regular state updates
+> _uiState.updateState { copy(value = newValue) }
+>
+> // For async operations
+> _uiState.updateStateWith(viewModelScope) {
+> repository.someAsyncOperation()
+> }
+> ```
+
+## Design Philosophy
+
+This codebase prioritizes pragmatic simplicity over theoretical purity, making conscious tradeoffs that favor maintainability and readability over absolute correctness or flexibility. Here are some key examples of this philosophy:
+
+### Centralized State Management
+
+#### Simplified Error and Loading Handling
+
+Instead of implementing error and loading states individually for each screen, we handle these centrally through the `StatefulComposable`:
```kotlin
-/**
- * BluetoothManager interface provides methods to manage Bluetooth connections.
- */
-interface BluetoothManager {
- /**
- * Attempts to establish a Bluetooth connection with the specified device address.
- *
- * @param address The address of the Bluetooth device to connect to.
- * @return A [Result] indicating the success or failure of the connection attempt.
- */
- suspend fun connect(address: String): Result
-
- /**
- * Returns the state of the connected Bluetooth device.
- *
- * @return A [StateFlow] emitting the current state of the connected Bluetooth device.
- */
- fun getConnectedDeviceState(): StateFlow
-
- /**
- * Closes the existing Bluetooth connection.
- *
- * @return A [Result] indicating the success or failure of closing the connection.
- */
- suspend fun closeConnection(): Result
+@Composable
+fun StatefulComposable(
+ state: UiState,
+ onShowSnackbar: suspend (String, String?) -> Boolean,
+ content: @Composable (T) -> Unit
+) {
+ content(state.data)
+
+ if (state.loading) {
+ // Centralized loading indicator
+ }
+
+ state.error.getContentIfNotHandled()?.let { error ->
+ // Centralized error handling
+ }
}
```
-It also contains several utilities and extension functions to make repetitive tasks easier. For example:
+**Tradeoff:**
+- ✅ **Simplicity**: UI components only need to focus on their happy path
+- ✅ **Consistency**: Error and loading states behave uniformly across the app
+- ❌ **Flexibility**: Less control over specific error/loading UI for individual screens
+
+### Direct State Management in ViewModels
+
+While the NowInAndroid codebase promotes a functional approach using Flow operators and transformations, we opt for a more direct approach using MutableStateFlow:
```kotlin
-/**
- * Displays a short toast message.
- *
- * @param message The message to be displayed in the toast.
- */
-fun Context.showToast(message: String) {
- Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
+// Our simplified approach
+class AuthViewModel @Inject constructor(
+ private val authRepository: AuthRepository,
+) : ViewModel() {
+ private val _authUiState = MutableStateFlow(UiState(AuthScreenData()))
+ val authUiState = _authUiState.asStateFlow()
+
+ fun updateEmail(email: String) {
+ _authUiState.updateState {
+ copy(
+ email = TextFiledData(
+ value = email,
+ errorMessage = if (email.isEmailValid()) null else "Email Not Valid"
+ )
+ )
+ }
+ }
}
+```
+
+**Tradeoff:**
+- ✅ **Readability**: State changes are explicit and easy to trace
+- ✅ **Simplicity**: Easier to manage multiple UI events and loading states
+- ✅ **Debuggability**: Direct state mutations are easier to debug
+- ❌ **Purity**: Less adherence to functional programming principles
+- ❌ **Resource Management**: No automatic cleanup of subscribers when the app is in background (compared to `SharingStarted.WhileSubscribed(5_000)`)
+
+> [!NOTE]
+> These patterns are guidelines, not rules. The goal is to make the codebase more maintainable and easier to understand, not to restrict flexibility where it's truly needed.
+
+
+## Getting Started
+
+### Prerequisites
+- Android Studio Hedgehog or newer
+- JDK 21
+- Firebase account for authentication and crashlytics
+
+### Initial Setup
+
+1. Clone and open project:
+```bash
+git clone https://github.com/atick-faisal/Jetpack-Compose-Starter.git
+```
+
+2. Firebase setup:
+- Create project in Firebase Console
+- Download `google-services.json` to `app/`
+- Add SHA fingerprint to Firebase Console for Google Sign-In:
+```bash
+./gradlew signingReport
+```
+
+> [!NOTE]
+> Firebase authentication and crashlytics requires Firebase console setup and the `google-services.json` file. I have provided a template to ensure a successful build. However, you need to provide your own in order to use all the functionalities.
+
+
+3. Debug builds:
+```bash
+./gradlew assembleDebug
+```
+
+### Release Setup
+
+1. Create `keystore.properties` in project root:
+```properties
+storePassword=****
+keyPassword=****
+keyAlias=****
+storeFile=keystore-file-name.jks
+```
+
+2. Place keystore file in `app/`
+
+3. Build release:
+```bash
+./gradlew assembleRelease
+```
+
+## Adding a New Feature: Step-by-Step Guide
+
+This guide walks through the process of adding a new feature to the app, following the established patterns and conventions.
+
+### Step 1: Define Data Models
+
+Start by defining your data models in the appropriate layer:
+
+1. **Network Models** (if feature requires API calls):
+```kotlin
+// network/src/main/kotlin/dev/atick/network/models/
+@Serializable
+data class NetworkFeatureData(
+ val id: Int,
+ val title: String
+)
+```
-/**
- * Checks if the app has a given permission.
- *
- * @param permission The permission to check.
- * @return `true` if the permission is granted, `false` otherwise.
- */
-fun Context.hasPermission(permission: String): Boolean {
- return ContextCompat.checkSelfPermission(this, permission) ==
- PackageManager.PERMISSION_GRANTED
+2. **UI Models** (what your screen will display):
+```kotlin
+// feature/src/main/kotlin/dev/atick/feature/models/
+data class FeatureScreenData(
+ val title: String,
+ val description: String = "",
+ // ... other UI state
+)
+```
+
+### Step 2: Create Data Source
+
+1. **Define the interface**:
+```kotlin
+// feature/src/main/kotlin/dev/atick/feature/data/
+interface FeatureDataSource {
+ suspend fun getFeatureData(): List
+ fun observeFeatureData(): Flow>
}
+```
-/**
- * Checks if all the given permissions are granted.
- *
- * @param permissions List of permissions to check.
- * @return `true` if all permissions are granted, `false` otherwise.
- */
-fun Context.isAllPermissionsGranted(permissions: List): Boolean {
- return permissions.all { hasPermission(it) }
+2. **Implement the data source**:
+```kotlin
+class FeatureDataSourceImpl @Inject constructor(
+ private val api: FeatureApi,
+ @IoDispatcher private val ioDispatcher: CoroutineDispatcher
+) : FeatureDataSource {
+ override suspend fun getFeatureData(): List {
+ return withContext(ioDispatcher) {
+ api.getFeatureData()
+ }
+ }
+
+ override fun observeFeatureData(): Flow> {
+ return flow {
+ // Implementation
+ }.flowOn(ioDispatcher)
+ }
}
```
-## Technologies
+### Step 3: Create Repository
-- Kotlin 2.0
-- Jetpack Compose
-- Kotlin Coroutines
-- Kotlin Flow for Reactive Data
-- Retrofit and OkHttp
-- Firebase Auth
-- Firebase Crashlytics
-- Room Database
-- Preferences Datastore
-- Dependency Injection with Hilt
-- Gradle Kotlin DSL
-- Gradle Version Catalog
-- Convention Plugin
+1. **Define repository interface**:
+```kotlin
+// feature/src/main/kotlin/dev/atick/feature/repository/
+interface FeatureRepository {
+ suspend fun getFeatureData(): Result>
+}
+```
-## Architecture
+2. **Implement repository**:
+```kotlin
+class FeatureRepositoryImpl @Inject constructor(
+ private val dataSource: FeatureDataSource
+) : FeatureRepository {
+ override suspend fun getFeatureData(): Result> =
+ suspendRunCatching {
+ dataSource.getFeatureData().map { it.toFeatureData() }
+ }
+}
+```
-This template follows the [official architecture guidance](https://developer.android.com/topic/architecture) suggested by Google.
+### Step 4: Create ViewModel
-## Modularization
+```kotlin
+// feature/src/main/kotlin/dev/atick/feature/ui/
+@HiltViewModel
+class FeatureViewModel @Inject constructor(
+ private val repository: FeatureRepository
+) : ViewModel() {
+ private val _uiState = MutableStateFlow(UiState(FeatureScreenData()))
+ val uiState = _uiState.asStateFlow()
+
+ init {
+ loadData()
+ }
+
+ private fun loadData() {
+ _uiState.updateStateWith(viewModelScope) {
+ repository.getFeatureData()
+ .map { data -> /* transform to screen data */ }
+ }
+ }
+
+ fun onUserAction(/* params */) {
+ _uiState.updateState {
+ copy(/* update state */)
+ }
+ }
+}
+```
-![Modularization](https://github.com/atick-faisal/Jetpack-Compose-Starter/blob/main/assets/modularization.svg)
+### Step 5: Create UI Components
-## Building
+1. **Create screen composable**:
+```kotlin
+// feature/src/main/kotlin/dev/atick/feature/ui/
+@Composable
+fun FeatureRoute(
+ onShowSnackbar: suspend (String, String?) -> Boolean,
+ viewModel: FeatureViewModel = hiltViewModel()
+) {
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+ StatefulComposable(
+ state = uiState,
+ onShowSnackbar = onShowSnackbar
+ ) { screenData ->
+ FeatureScreen(
+ screenData = screenData,
+ onAction = viewModel::onUserAction
+ )
+ }
+}
+
+@Composable
+private fun FeatureScreen(
+ screenData: FeatureScreenData,
+ onAction: () -> Unit
+) {
+ // UI implementation
+}
+```
-### Debug
+2. **Add preview**:
+```kotlin
+@DevicePreviews
+@Composable
+private fun FeatureScreenPreview() {
+ FeatureScreen(
+ screenData = FeatureScreenData(/* sample data */),
+ onAction = {}
+ )
+}
+```
-This project requires Firebase for analytics. Building the app requires `google-services.json` to be present inside the `app` dir. This file can be generated from the [Firebase Console](https://firebase.google.com/docs/android/setup). After that, run the following from the terminal.
+### Step 6: Setup Navigation
-```sh
-$ ./gradlew assembleDebug
+1. **Define navigation endpoints**:
+```kotlin
+// feature/src/main/kotlin/dev/atick/feature/navigation/
+@Serializable
+data object FeatureNavGraph
+@Serializable
+data object Feature
```
-Or, use `Build > Rebuild Project`.
+2. **Add navigation extensions**:
+```kotlin
+fun NavController.navigateToFeature(navOptions: NavOptions? = null) {
+ navigate(Feature, navOptions)
+}
-### Release
+fun NavGraphBuilder.featureScreen(
+ onShowSnackbar: suspend (String, String?) -> Boolean
+) {
+ composable {
+ FeatureRoute(
+ onShowSnackbar = onShowSnackbar
+ )
+ }
+}
-Building the `release` version requires a `Keystore` file in the `app` dir. Also, a `keystore.properties` file needs to be created in the `rootDir`.
+fun NavGraphBuilder.featureNavGraph(
+ nestedGraphs: NavGraphBuilder.() -> Unit
+) {
+ navigation(
+ startDestination = Feature
+ ) {
+ nestedGraphs()
+ }
+}
+```
+### Step 7: Setup Dependency Injection
+
+1. **Add module for data source**:
+```kotlin
+@Module
+@InstallIn(SingletonComponent::class)
+abstract class DataSourceModule {
+ @Binds
+ @Singleton
+ abstract fun bindFeatureDataSource(
+ impl: FeatureDataSourceImpl
+ ): FeatureDataSource
+}
```
-storePassword=****
-keyPassword=*****
-keyAlias=****
-storeFile=keystore file name (e.g., key.jks)
+
+2. **Add module for repository**:
+```kotlin
+@Module
+@InstallIn(SingletonComponent::class)
+abstract class RepositoryModule {
+ @Binds
+ @Singleton
+ abstract fun bindFeatureRepository(
+ impl: FeatureRepositoryImpl
+ ): FeatureRepository
+}
```
-After that, run the following from the terminal.
+## Final Checklist
-```sh
-$ ./gradlew assembleRelease
-```
+✅ Data models defined
+✅ Data source interface and implementation created
+✅ Repository interface and implementation created
+✅ ViewModel handling state and user actions
+✅ UI components with previews
+✅ Navigation setup
+✅ Dependency injection modules
+
+
+## Documentation
+
+
+
+
+
+ Read The Documentation Here
+
Qatar University Machine Learning Group