diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
index f57bac3..e744d44 100644
--- a/.github/workflows/android.yml
+++ b/.github/workflows/android.yml
@@ -3,7 +3,7 @@ name: Android Debug CI/CD Rho.Studio®
on:
workflow_dispatch:
push:
- branches: [ " " ]
+ branches: [ "37-data-layer-and-dagger-di" ]
pull_request:
branches: [ "dev" , "pre-release" ]
@@ -23,6 +23,12 @@ jobs:
distribution: 'temurin'
cache: gradle
+ # Add this step here
+ - name: Decode Google Services JSON
+ env:
+ GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }}
+ run: echo $GOOGLE_SERVICES_JSON | base64 --decode > app/google-services.json
+
- name: Grant execute permission for gradlew
run: chmod +x gradlew
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index c586309..00a2493 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -3,7 +3,7 @@ name: Android Release Rho.Studio®
on:
workflow_dispatch:
push:
- branches: [ "Pre-release-v102" ]
+ branches: [ "pre-release-v103" ]
pull_request:
branches: [ "main" ]
@@ -23,6 +23,12 @@ jobs:
distribution: 'temurin'
cache: gradle
+ # Add this step here
+ - name: Decode Google Services JSON
+ env:
+ GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }}
+ run: echo $GOOGLE_SERVICES_JSON | base64 --decode > app/google-services.json
+
- name: Grant execute permission for gradlew
run: chmod +x gradlew
diff --git a/.gitignore b/.gitignore
index 8773f8b..1fc891c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,3 +30,4 @@ replay_pid*
.idea/
.gradle/
build/
+/app/google-services.json
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..a1e5038
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,90 @@
+# Architecture & Contribution Guide: Rho Studio UI
+
+## 1. Project Vision & Architecture
+Rho Studio UI is a modern Android application built with **Jetpack Compose** and **MVVM** following a **Single-Activity Architecture**.
+
+To achieve scalability, implement **Domain-Driven Design (DDD)** and **Clean Architecture** principles. This ensures a clear separation of concerns, framework independence, and high testability.
+
+---
+
+## 2. Feature Implementation Workflow (Step-by-Step)
+
+When adding a new feature (e.g., "Settings", "Profile"), follow this **Inside-Out** sequence to ensure architectural integrity:
+
+### Step 1: Domain Layer (The Logic)
+1. **Define Models**: Create pure Kotlin data classes in `core:domain` (e.g., `Settings.kt`).
+2. **Define Repository Interface**: Add an interface in `core:domain` describing the data contract.
+3. **Create Interactor (UseCase)**: Implement the business logic by inheriting from `BaseUseCase
`.
+ * **P (Parameters)**: Use a `data class` for multiple inputs or `Unit` for none.
+ * **R (Return)**: The raw data type (Dagger/BaseUseCase will wrap it in `Result`).
+ * **Rule**: Must be a pure Kotlin class without Android dependencies.
+ * **Rule**: Must be testable with MockK/JUnit 5.
+
+Example:
+```kotlin
+class LoginUseCase @Inject constructor(
+ private val repository: AuthRepository
+) : BaseUseCase() {
+ override suspend fun execute(parameters: Credentials): User {
+ return repository.login(parameters)
+ }
+}
+```
+
+### Step 2: Data Layer (The Infrastructure)
+1. **Implement Repository**: Create the implementation in `core:data` using Firebase, Retrofit, or DataStore.
+2. **Dagger Binding**: Add a `@Binds` method in `CoreModule.kt` to link the interface to the implementation.
+
+### Step 3: UI Layer (The Presentation)
+1. **Create ViewModel**: Inherit from `BaseViewModel`.
+ * Use `launchSafe` or `launchWithLoading` for all coroutines.
+ * Expose UI state via `StateFlow`.
+2. **Dagger Multibinding**:
+ * Create a Dagger `@Module` for the feature.
+ * Use `@Binds @IntoMap @ViewModelKey(MyViewModel::class)` to register it.
+ * Add the module to the appropriate component (`AppComponent` for public, `UserComponent` for authenticated).
+3. **Build Composables**: Create stateless Compose functions. Observe the ViewModel state in the screen-level Composable.
+
+---
+
+## 3. Dependency Injection Standards (Dagger 2)
+
+We use a **Multi-Tiered Dependency Graph**. Developers must respect scope boundaries:
+
+* **@Singleton**: For infrastructure (Network, Firebase, SessionManager). Lives in `CoreComponent`.
+* **@AppScope**: For public/login logic. Lives in `AppComponent`.
+* **@UserScope**: For authenticated user data. Lives in `UserComponent`.
+
+> [!WARNING]
+> Never attempt to inject a `@UserScope` dependency into a `@Singleton` class. This will cause a memory leak or a crash.
+
+---
+
+## 4. UI Standards & Base Classes
+
+### 4.1 BaseViewModel
+Every ViewModel **must** extend `BaseViewModel`. This provides:
+- `isLoading`: A built-in StateFlow for progress bars.
+- `launchSafe { ... }`: Automatic error handling and crash prevention.
+- `handleError(e)`: Standardized toast and error state management.
+
+### 4.2 Stateless Composables
+Divide your UI into two parts:
+1. **Screen Composable**: "Stateful." Injects the ViewModel and passes data down.
+2. **Component Composables**: "Stateless." Take raw data and lambdas (e.g., `onClick: () -> Unit`). This makes them previewable and testable.
+
+---
+
+## 5. Security & PII
+- **PII**: Any Personal Identifiable Information must be stored in the **Encrypted DataStore**.
+- **Session**: Global session state is managed by `SessionManager`. Use it to reactively hide/show UI elements based on authentication.
+
+---
+
+## 6. Testing Requirements
+- **Domain**: 90%+ coverage for UseCases.
+- **ViewModels**: Test state transitions using `Dispatcher.Main` delegation.
+- **Data**: Mock external SDKs (Firebase) using MockK.
+
+---
+**[Rho.Studio®](https://rho.studio/) - Engineering Department** - Contact [alexis.tercero@rho.studio](mailto:alexis.tercero@rho.studio)
diff --git a/CONTRIBUTION.md b/CONTRIBUTION.md
deleted file mode 100644
index 3ea1c6a..0000000
--- a/CONTRIBUTION.md
+++ /dev/null
@@ -1,70 +0,0 @@
-# Architecture & Contribution Guide: Rho Studio UI
-
-## 1. Project Vision & Architecture
-Rho Studio UI is a modern Android application built with **Jetpack Compose** and **MVVM** following a **Single-Activity Architecture**.
-
-To achieve enterprise-grade scalability, we strictly implement **Clean Architecture** principles. This ensures a clear separation of concerns, framework independence, and high testability.
-
----
-
-## 2. Clean Architecture Layer Responsibilities
-All contributions must respect the strict boundaries between the following three layers:
-
-### 2.1 The UI Layer (`features/` & `ui/`)
-* **Role**: Handles user interaction and data presentation.
-* **Components**:
- * **Compose Screens/Components**: Purely declarative and stateless. They observe state and emit events.
- * **ViewModels**: Act as a bridge. They manage UI state (Loading, Error, Toast) and handle user intent by calling Use Cases.
-* **Boundary Rule**: Never contains business logic. Never interacts directly with Repositories.
-
-### 2.2 The Domain Layer (`core/domain/`)
-* **Role**: The "Heart" of the application. Contains the essential business rules.
-* **Components**:
- * **Use Cases (Interactors)**: Classes like `LoginUseCase.kt` that encapsulate a single, atomic business transaction.
- * **Domain Models**: Pure data entities (e.g., `User.kt`) that are framework-independent.
-* **Boundary Rule**: **Pure Kotlin only**. Must not import `android.*` or depend on any external libraries/frameworks (except pure Kotlin ones). This layer is the "Single Source of Truth" for *logic*.
-
-### 2.3 The Data Layer (`core/data/`)
-* **Role**: Manages data acquisition and persistence.
-* **Components**:
- * **Repositories**: Implementation of data fetching (API, Room, Preferences).
- * **Managers**: State holders like `SessionManager.kt` that coordinate global app state.
-* **Boundary Rule**: Acts as the "Single Source of Truth" for *data state*. It implements the requirements defined by the Domain layer.
-
----
-
-## 3. Core Requirements for Contributions
-
-### 3.1 MVVM & UDF (Unidirectional Data Flow)
-- **State flows down**: From ViewModel to Composables.
-- **Events flow up**: From UI to ViewModel via lambdas.
-
-### 3.2 Single-Activity & Reactive Navigation
-- **MainActivity** is the sole navigation orchestrator.
-- **ViewModels** and **Use Cases** must **never** hold a `NavController` or trigger navigation directly.
-- **Logic**: Use Cases update the session/state in the Data layer. `MainActivity` observes this state and performs the transition (e.g., auto-routing to Login on session expiry).
-
----
-
-## 4. Implementing New Features (Profile, Feed, Chat)
-
-Every new feature should be built following the **Inside-Out** approach:
-
-1. **Inside (Domain)**: Create the `UseCase` (e.g., `UpdateProfileUseCase`, `GetFeedUseCase`, `SendMessageUseCase`).
- - Use the `Result` wrapper for success/failure.
- - Write a Unit Test for the logic.
-2. **Middle (ViewModel)**: Create the bridge that transforms the Use Case `Result` into observable UI state.
-3. **Outside (UI)**: Build the stateless Compose UI.
- - **Feed UI**: Use `LazyColumn` for efficiency. Implement a stateless `PostItem.kt`.
- - **Chat UI**: Implement specialized "Message Bubble" components. Input fields must update the ViewModel state immediately.
-
----
-
-## 5. Technical Constraints
-- **Atomic Transactions**: Multi-step actions (e.g., validate -> save -> sync) must be managed as a single atomic unit within a `UseCase`.
-- **Framework Independence**: Keep the Domain layer free of Android dependencies to support future Gradle modularization.
-- **Standardized Results**: Always return `Result.Success`, `Result.Error`, or `Result.Loading` from Use Cases.
-
----
-**[Rho.Studio®](https://rho.studio/) - Engineering Department** - Contact [alexis.tercero@rho.studio](mailto:alexis.tercero@rho.studio)
-
diff --git a/README.md b/README.md
index 8558d41..f3b030e 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@ An Android Jetpack Compose app.
[](https://github.com/Rho-Studio/UI-Utils-Rho-Studio/actions/workflows/android.yml)
[](https://github.com/Rho-Studio/UI-Utils-Rho-Studio/actions/workflows/release.yml)
-> _Document Version: 2.0 Last Updated: August 10, 2026_
+> _Document Version: 3.1 Last Updated: August 25, 2026_
## Enterprise-Grade Android Architecture with Jetpack Compose
This document provides a comprehensive technical overview of the **Rho Studio UI** application. It serves as the primary architectural reference for developers, outlining the system's design, layer responsibilities, and technical standards.
@@ -16,12 +16,17 @@ This document provides a comprehensive technical overview of the **Rho Studio UI
## 1. Executive Summary
Rho Studio UI is the **base application template** designed to establish and enforce the **Rho Studio Android App Standards**. It provides a robust foundation for building secure, authenticated mobile experiences within the Rho Studio ecosystem.
-The application is a pure **Jetpack Compose** implementation following a **Single-Activity Architecture**, leveraging a reactive **MVVM (Model-View-ViewModel)** pattern to ensure a clean separation of concerns, testability, and a fluid user experience driven by **Unidirectional Data Flow (UDF)**. This architectural foundation ensures a focus on **Fluid UX**, **Transactional Integrity**, and **Decoupled Business Logic**.
+The application is a **Jetpack Compose** implementation following a **Single-Activity Architecture**, leveraging a reactive **MVVM (Model-View-ViewModel)** pattern, implementing a Multi-Tier Dagger Hierarchy and a fluid user experience driven by **Unidirectional Data Flow (UDF)**. This architectural foundation ensures a focus on **Fluid UX**, **Transactional Integrity**, and **Decoupled Business Logic**.
+
+> [!IMPORTANT]
+> **In order to add a new feature**: See [CONTRIBUTING.md](CONTRIBUTING.md).
### Core Features:
-- **Secure Authentication**: Robust login flow with real-time validation and session lifecycle management, following corporate security protocols.
-- **Adaptive Home Experience**: A responsive home interface that dynamically adjusts to different service modules and device form factors.
-- **Brand Consistency**: A centralized design system leveraging Material 3 to reflect the Rho Studio corporate identity across all derived applications.
+- **Authentication Orchestration**: Implements a production flow using Firebase Auth with a reactive session lifecycle. It ensures transactional security by synchronizing remote authentication states with automated, state-driven navigation transitions.
+- **Architectural Boundaries**: A high-performance Multi-Tier Dagger Hierarchy that enforces strict data isolation between core, app, and user scopes. Sensitive user information is isolated within a dedicated "User Tier" that is physically purged from memory upon logout to prevent data leakage.
+- **Reactive Data Layer and State Integrity**: Leverages Jetpack DataStore for atomic persistence and a Sealed State Machine for global orchestration. This creates a thread-safe, non-blocking "stream of truth" that guarantees the UI remains a perfect reflection of the underlying data.
+- **Stateless Presentation Layer**: A fully decoupled UI built with Jetpack Compose following Unidirectional Data Flow (UDF) principles. This enables the presentation layer to scale dynamically across diverse device form factors while ensuring high testability and visual consistency.
+- **Domain-Driven Design (DDD)**: Every business operation is encapsulated in a dedicated UseCase (Interactor) within a framework-independent domain layer. By strictly isolating the business rules from the Android framework, ensure testability, logic reusability, and architectural resilience against framework changes.
---
@@ -29,39 +34,53 @@ The application is a pure **Jetpack Compose** implementation following a **Singl
The application follows a **Single-Activity Architecture** and is structured according to **Clean Architecture** principles. It utilizes a **Feature-Layered Modularization** strategy to ensure scalability and maintainability.
### 2.1 Layered Structure
-The system is divided into three primary logical layers, enforcing a strict unidirectional dependency flow: **UI → Domain ← Data**.
+The system follows the three layers Google's recommendations:
```mermaid
graph TD
- subgraph "UI Layer (Presentation)"
- UI[Jetpack Compose Screens]
+ subgraph UI["UI Layer (Presentation)"]
+ UI_Screens[Jetpack Compose Screens]
VM[ViewModels]
Nav[Navigation / NavHost]
end
- subgraph "Domain Layer (Business Logic)"
+ subgraph Domain["Domain Layer (Business Logic)"]
UC[Use Cases / Interactors]
Entities[Domain Entities]
Int[Repository Interfaces]
end
- subgraph "Data Layer (Infrastructure)"
+ subgraph Data["Data Layer (Infrastructure)"]
Repo[Repository Implementations]
- SM[Session Manager]
+ SM[Session Manager / SSOT]
Local[Local / Network Data Sources]
end
- UI --> VM
+ UI_Screens --> VM
VM --> UC
UC --> Entities
UC --> Int
Repo -.-> Int
Repo --> SM
Repo --> Local
+
+ linkStyle default stroke:#D32F2F,stroke-width:2px
+
+ classDef uiNode fill:#4A4A4A,stroke:#D32F2F,color:#FFFFFF
+ classDef domainNode fill:#333333,stroke:#D32F2F,color:#FFFFFF
+ classDef dataNode fill:#333333,stroke:#D32F2F,color:#FFFFFF
+
+ class UI_Screens,VM,Nav uiNode
+ class UC,Entities,Int domainNode
+ class Repo,SM,Local dataNode
+
+ style UI fill:#333333,stroke:#D32F2F,color:#FFFFFF
+ style Domain fill:#4A4A4A,stroke:#333333,color:#FFFFFF
+ style Data fill:#D3D3D3,stroke:#D32F2F,color:#000000
```
### 2.2 Multi-Module Topology
-We have moved away from a monolithic `:app` structure to a **Feature-Layered Modularization** strategy. This optimizes build parallelization and enforces strict dependency inversion.
+The project is split into granular Gradle modules to improve build parallelization and enforce boundaries.
```mermaid
flowchart TD
APP[":app
MainActivity, NavHost"]
@@ -84,15 +103,169 @@ flowchart TD
UI_CORE --> DOMAIN
DOMAIN -.->|"implemented by"| DATA
+
+ linkStyle default stroke:#D32F2F,stroke-width:2px
+
+ classDef appNode fill:#D32F2F,stroke:#FFFFFF,color:#FFFFFF
+ classDef featureNode fill:#4A4A4A,stroke:#D32F2F,color:#FFFFFF
+ classDef coreNode fill:#333333,stroke:#D32F2F,color:#FFFFFF
+ classDef domainNode fill:#333333,stroke:#D32F2F,color:#FFFFFF
+ classDef dataNode fill:#333333,stroke:#D32F2F,color:#FFFFFF
+
+ class APP appNode
+ class AUTH,HOME featureNode
+ class UI_CORE coreNode
+ class DOMAIN domainNode
+ class DATA dataNode
+
+ style APP fill:#D32F2F,stroke:#FFFFFF,color:#FFFFFF
+ style AUTH fill:#4A4A4A,stroke:#D32F2F,color:#FFFFFF
+ style HOME fill:#4A4A4A,stroke:#D32F2F,color:#FFFFFF
+ style UI_CORE fill:#333333,stroke:#D32F2F,color:#FFFFFF
+ style DOMAIN fill:#4A4A4A,stroke:#333333,color:#FFFFFF
+ style DATA fill:#D3D3D3,stroke:#D32F2F,color:#000000
+```
+> [!Tip]
+> `:features` depend only on `:core` modules (`:core:domain`, `:core:ui`), preventing circular dependencies. Feature-specific models remain within their respective feature modules.
+
+### 2.3 Multi-Tier Dependency Injection (Dagger 2 + KSP)
+We utilize a high-performance Directed Acyclic Graph (DAG) generated at compile-time using KSP to ensure zero runtime overhead. The graph is organized into three tiers to mirror the application lifecycle:
+
+#### DI Conceptual Framework
+
+| Concept | Implementation | Purpose |
+| :--- | :--- | :--- |
+| **The Request** | `@Inject` constructor | Objects request dependencies via constructor injection, ensuring loose coupling and testability |
+| **The Recipe** | `@Module` with `@Provides` / `@Binds` | Dagger Modules define how to provide complex objects, interfaces, or library classes |
+| **The Manager** | `@Component` | Bridge between providers (Modules) and consumers (Activities/ViewModels). Validates graph at compile-time |
+| **The Lifecycle** | `@Scope` (e.g., `@Singleton`, `@UserScope`) | Ensures objects live exactly as long as their context (App lifecycle vs. User session) |
+| **Annotation Retention** | `@Retention(AnnotationRetention.RUNTIME)` | Ensures annotation metadata is available to Dagger compiler and at runtime |
+| **Multibinding Keys** | `@MapKey` | Identifies which class type to use as a Key in Dagger's internal Maps |
+| **Lazy Provisioning** | `Provider` | Defers actual creation of dependencies until requested, saving memory and startup time |
+| **Kotlin Interop** | `@JvmSuppressWildcards` | Handles Kotlin's generic covariance in Dagger's Java-based compiler |
+
+#### Multi-Tier Component Dependency Architecture
+
+```mermaid
+flowchart TB
+ subgraph Core["CoreComponent (@Singleton)"]
+ direction TB
+ CTX["Context (Application)"]
+ DS["DataStore"]
+ SM["SessionManager"]
+ FA["FirebaseAuth"]
+ FAN["FirebaseAnalytics"]
+ end
- style APP fill:#e94560,stroke:#c62828,color:#ffffff
- style AUTH fill:#1a1a2e,stroke:#e94560,color:#ffffff
- style HOME fill:#1a1a2e,stroke:#e94560,color:#ffffff
- style UI_CORE fill:#16213e,stroke:#0f3460,color:#ffffff
- style DOMAIN fill:#0f3460,stroke:#16213e,color:#ffffff
- style DATA fill:#1a1a2e,stroke:#e94560,color:#ffffff
+ subgraph App["AppComponent (@AppScope)"]
+ direction TB
+ AM["AuthModule"]
+ UM["UIModule"]
+ LoginVM["LoginViewModel"]
+ end
+
+ subgraph User["UserComponent (@UserScope)"]
+ direction TB
+ HM["HomeModule"]
+ UM2["UIModule (Reused)"]
+ HomeVM["HomeViewModel"]
+ HeaderVM["HeaderViewModel"]
+ end
+
+ Core -->|"Component Dependency"| App
+ Core -->|"Component Dependency"| User
+
+ AM -->|"Contains"| LoginVM
+ AM -->|"Contains"| UM
+
+ HM -->|"Contains"| HomeVM
+ HM -->|"Contains"| HeaderVM
+ HM -->|"Contains"| UM2
+
+ linkStyle default stroke:#D32F2F,stroke-width:2px
+
+ classDef coreNode fill:#4A4A4A,stroke:#D32F2F,color:#FFFFFF
+ classDef appNode fill:#333333,stroke:#D32F2F,color:#FFFFFF
+ classDef userNode fill:#333333,stroke:#D32F2F,color:#FFFFFF
+
+ class CTX,DS,SM,FA,FAN coreNode
+ class AM,UM,LoginVM,HeaderVM appNode
+ class HM,UM2,HomeVM,HeaderVM2 userNode
+
+ style Core fill:#333333,stroke:#D32F2F,color:#FFFFFF
+ style App fill:#4A4A4A,stroke:#333333,color:#FFFFFF
+ style User fill:#D3D3D3,stroke:#D32F2F,color:#000000
```
-> **Key Principle**: `:features` depend only on `:core` modules (`:core:domain`, `:core:ui`), preventing circular dependencies. Feature-specific models remain within their respective feature modules, adhering to the Interface Segregation Principle.
+
+**Core Tier** (`CoreComponent`):
+
+- **Scope**: `@Singleton`.
+- **Responsibility**: Infrastructure foundation (Firebase, DataStore, Threading).
+- **Hierarchy**: The foundation; does not depend on other components.
+
+**App Tier** (`AppComponent`):
+
+- **Scope**: @AppScope.
+- **Responsibility**: Public lifecycle (Authentication feature, Shared UI).
+- **Hierarchy**: Depends on CoreComponent. Orchestrates AuthModule (Pre-Login ViewModels) and UIModule (Shared ViewModels).
+
+**User Tier** (`UserComponent`):
+
+- **Scope**: `@UserScope`.
+- **Responsibility**: Authenticated session (Home feature, Profile).
+- **Hierarchy**: Depends on CoreComponent. Orchestrates `HomeModule` (Post-Login ViewModels) and `UIModule`.
+- **Isolation**: Created dynamically upon login and binary-purged from memory upon logout to ensure session security.
+
+### 2.4 ViewModel Multibinding Strategy
+
+To decouple the UI from DI wiring, we implement a centralized registry using `@IntoMap`:
+
+```mermaid
+flowchart TB
+ subgraph Contribution["Step 1: Contribution"]
+ M1["AuthModule"] --> B1["@Binds LoginViewModel"]
+ M2["UIModule"] --> B2["@Binds HeaderViewModel"]
+ M3["HomeModule"] --> B3["@Binds HomeViewModel"]
+ end
+
+ subgraph Aggregation["Step 2: Aggregation"]
+ M1 --> Map["Internal Map
Map>"]
+ M2 --> Map
+ M3 --> Map
+ end
+
+ subgraph Resolution["Step 3: Resolution"]
+ Map --> Factory["DaggerViewModelFactory"]
+ Factory --> UI["UI requests ViewModel by Class"]
+ end
+
+ Contribution --> Aggregation --> Resolution
+
+ linkStyle default stroke:#D32F2F,stroke-width:2px
+
+ classDef contributionNode fill:#4A4A4A,stroke:#D32F2F,color:#FFFFFF
+ classDef aggregationNode fill:#333333,stroke:#D32F2F,color:#FFFFFF
+ classDef resolutionNode fill:#333333,stroke:#D32F2F,color:#FFFFFF
+
+ class M1,B1,M2,B2,M3,B3 contributionNode
+ class Map aggregationNode
+ class Factory,UI resolutionNode
+
+ style Contribution fill:#333333,stroke:#D32F2F,color:#FFFFFF
+ style Aggregation fill:#4A4A4A,stroke:#333333,color:#FFFFFF
+ style Resolution fill:#D3D3D3,stroke:#D32F2F,color:#000000
+```
+
+#### Key Principles:
+
+- **Registry**: Feature modules contribute ViewModels via a custom @ViewModelKey.
+- **Factory**: A single DaggerViewModelFactory resolves instances on-demand, adhering to the Open/Closed Principle.
+- **Isolation**: Each component builds unique internal Maps, ensuring strict data and logic isolation based on user state.
+
+| Component | Modules Included | Resulting Internal Map |
+| :--- | :--- | :--- |
+| **AppComponent** | `AuthModule`, `UIModule` | `{LoginViewModel, HeaderViewModel}` |
+| **UserComponent** | `HomeModule`, `UIModule` | `{HomeViewModel, HeaderViewModel}` |
---
@@ -116,20 +289,19 @@ flowchart TB
end
subgraph Shared["Shared UI Components"]
- PV["BaseViewModel.kt
- Loading states
- Error handling"]
+ PV["BaseViewModel.kt
- launchSafe
- isLoading state"]
HV["HeaderViewModel.kt
- Session state bridging"]
PH["PageHeader.kt"]
- PF["PageFooter.kt"]
end
subgraph Auth["Authentication Feature"]
LS["LoginScreen.kt"]
- LVM["LoginViewModel.kt
- Form state
- Debounced validation"]
+ LVM["LoginViewModel.kt"]
end
subgraph Home["Home Feature"]
HS["HomeScreen.kt"]
- HVM["HomeViewModel.kt
- Home state
- Session termination"]
+ HVM["HomeViewModel.kt"]
end
MA --> LS
@@ -139,34 +311,86 @@ flowchart TB
LVM --> PV
HVM --> PV
HV --> PV
-
- style Navigation fill:#e94560,stroke:#c62828,color:#ffffff
- style Shared fill:#16213e,stroke:#0f3460,color:#ffffff
- style Auth fill:#1a1a2e,stroke:#e94560,color:#ffffff
- style Home fill:#1a1a2e,stroke:#e94560,color:#ffffff
+
+ linkStyle default stroke:#D32F2F,stroke-width:2px
+
+ classDef navNode fill:#D32F2F,stroke:#FFFFFF,color:#FFFFFF
+ classDef sharedNode fill:#333333,stroke:#D32F2F,color:#FFFFFF
+ classDef authNode fill:#4A4A4A,stroke:#D32F2F,color:#FFFFFF
+ classDef homeNode fill:#4A4A4A,stroke:#D32F2F,color:#FFFFFF
+
+ class MA navNode
+ class PV,HV,PH sharedNode
+ class LS,LVM authNode
+ class HS,HVM homeNode
+
+ style Navigation fill:#D3D3D3,stroke:#D32F2F,color:#000000
+ style Shared fill:#4A4A4A,stroke:#333333,color:#FFFFFF
+ style Auth fill:#333333,stroke:#D32F2F,color:#FFFFFF
+ style Home fill:#333333,stroke:#D32F2F,color:#FFFFFF
```
### 3.2 Domain Layer (Business Logic)
**Goal**: House the platform-agnostic business rules and "truth" of the application.
- **Pure Kotlin**: This layer has zero dependencies on the Android Framework (no `Context`, no `Parcelable`).
-- **Use Cases (Interactors)**: Each business action is encapsulated in a dedicated Use Case (e.g., `LoginUseCase`). This promotes the Single Responsibility Principle and makes logic reusable across ViewModels.
- **Entities**: Data classes like `User` and `Credentials` represent the core business models.
+- **Use Cases (Interactors)**: Each business action is encapsulated in a dedicated Use Case (e.g., `LoginUseCase`). This promotes the Single Responsibility Principle and makes logic reusable across ViewModels.
+- **BaseUseCase Pattern**: All interactors inherit from `BaseUseCase`. This architectural anchor standardizes:
+ - **Thread Safety**: Automatic execution on `Dispatchers.IO`.
+ - **Result Wrapping**: Consistent use of the `Result` sealed class for Success/Error states.
+ - **Functional Invocation**: Use cases are invoked as functions using the `invoke` operator.
+
- **Key Components**:
- `BaseUseCase`: Standardizes execution context (Coroutines) and error handling.
- `SessionManagerInterface`: Defines the contract for session operations without revealing implementation details.
- `LoginUseCase`: Encapsulates the authentication transaction.
- `LogoutUseCase`: Orchestrates atomic session teardown.
+```mermaid
+flowchart TB
+ subgraph UseCases["Use Cases (Interactors)"]
+ LU["LoginUseCase
- Validate Credentials
- Authenticate via Firebase
- Commit to SSOT"]
+ LogU["LogoutUseCase
- Clear session
- Reset global state"]
+ VCU["ValidateCredentialsUseCase"]
+ RTU["RefreshTokenUseCase
(Planned)"]
+ end
+
+ subgraph Models["Domain Models"]
+ U["User.kt"]
+ C["Credentials.kt"]
+ AT["AuthToken.kt"]
+ end
+
+ subgraph Contracts["Repository Contracts"]
+ AR["AuthRepository"]
+ SR["SessionRepository"]
+ end
+
+ UseCases --> Models
+ UseCases --> Contracts
+
+ linkStyle default stroke:#D32F2F,stroke-width:2px
+
+ classDef useCaseNode fill:#333333,stroke:#D32F2F,color:#FFFFFF
+ classDef modelNode fill:#333333,stroke:#D32F2F,color:#FFFFFF
+ classDef contractNode fill:#333333,stroke:#D32F2F,color:#FFFFFF
+
+ class LU,LogU,VCU,RTU useCaseNode
+ class U,C,AT modelNode
+ class AR,SR contractNode
+
+ style UseCases fill:#4A4A4A,stroke:#333333,color:#FFFFFF
+ style Models fill:#333333,stroke:#D32F2F,color:#FFFFFF
+ style Contracts fill:#D3D3D3,stroke:#D32F2F,color:#000000
+```
+
### 3.3 Data Layer (Infrastructure)
**Goal**: Manage data acquisition, persistence, and external service coordination.
- **Repository Pattern**: Acts as a mediator between different data sources (Network, Database) and the Domain Layer.
- **Session Management**: `SessionManager` serves as the Single Source of Truth (SSOT) for the user's authentication state, exposing `StateFlow` for the UI to observe.
-- **Current Implementation**: Uses `SharedPreferences` with `Gson` serialization for persistence and mock authentication for development.
-- **Key Components**:
- - `SessionManager.kt`: Singleton coordinator for authentication state and user profile.
- - `SessionRepository.kt`: Coordinates data retrieval strategies.
- - `SessionRepositoryImpl.kt`: Manages persistent storage using SharedPreferences. Migrated to Room Database in the future.
- - `AuthRepositoryImpl.kt`: Mock implementation (**temporary**) simulating network delay and user creation. Replaced by Firebase Auth in the future.
+- **Production Sources**: Production-grade implementation using Firebase Auth and Jetpack DataStore.
+- **Telemetry**: Integrated Firebase Analytics for automated journey tracking.
+- **Dependency Inversion**: Repository interfaces defined in Domain; implementations in Data.
```mermaid
flowchart TB
subgraph SSOT["Single Source of Truth"]
@@ -174,51 +398,148 @@ flowchart TB
end
subgraph Repos["Repository Implementations"]
- ARI["AuthRepositoryImpl
- Mock login()"]
- SRI["SessionRepositoryImpl
- SharedPreferences"]
+ ARI["AuthRepositoryImpl
- Firebase Auth"]
+ SRI["SessionRepositoryImpl
- Jetpack DataStore"]
end
- subgraph Sources["Data Sources (Planned)"]
- Remote["Remote API
- Firebase Auth"]
- Local["Local Storage
- Room Database"]
+ subgraph Sources["Production Infrastructure"]
+ Remote["Firebase Auth SDK"]
+ Local["Jetpack DataStore PII"]
end
SM --> SRI
ARI --> Remote
SRI --> Local
+
+ linkStyle default stroke:#D32F2F,stroke-width:2px
+
+ classDef ssotNode fill:#D32F2F,stroke:#FFFFFF,color:#FFFFFF
+ classDef repoNode fill:#4A4A4A,stroke:#D32F2F,color:#FFFFFF
+ classDef sourceNode fill:#333333,stroke:#D32F2F,color:#FFFFFF
+
+ class SM ssotNode
+ class ARI,SRI repoNode
+ class Remote,Local sourceNode
+
+ style SSOT fill:#333333,stroke:#D32F2F,color:#FFFFFF
+ style Repos fill:#4A4A4A,stroke:#333333,color:#FFFFFF
+ style Sources fill:#D3D3D3,stroke:#D32F2F,color:#000000
+```
+#### Session State Machine
+
+```mermaid
+flowchart LR
+ INIT["Uninitialized"] -->|"initialize()"| CHECK["Checking"]
+ CHECK -->|"Valid token found"| AUTH["Authenticated"]
+ CHECK -->|"No token / expired"| GUEST["Guest"]
+ AUTH -->|"logout()"| GUEST
+ GUEST -->|"login()"| AUTH
+
+ linkStyle default stroke:#D32F2F,stroke-width:2px
+
+ classDef stateNode fill:#333333,stroke:#D32F2F,color:#FFFFFF
+
+ class INIT,CHECK,AUTH,GUEST stateNode
+```
+#### Authentication Flow Architecture
+```mermaid
+flowchart TB
+ subgraph UI["UI Layer"]
+ LS["LoginScreen"]
+ LVM["LoginViewModel"]
+ end
+
+ subgraph Domain["Domain Layer"]
+ LU["LoginUseCase"]
+ AR["AuthRepository
(Interface)"]
+ end
+
+ subgraph Data["Data Layer"]
+ ARI["AuthRepositoryImpl"]
+ FRD["FirebaseRemoteDataSource"]
+ ARD["AnalyticsRemoteDataSource"]
+ SM["SessionManager"]
+ end
+
+ subgraph Firebase["Firebase SDK"]
+ FA["FirebaseAuth"]
+ FAN["FirebaseAnalytics"]
+ end
- style SSOT fill:#e94560,stroke:#c62828,color:#ffffff
- style Repos fill:#1a1a2e,stroke:#e94560,color:#ffffff
- style Sources fill:#0f3460,stroke:#16213e,color:#ffffff
+ LS --> LVM
+ LVM --> LU
+ LU --> AR
+ AR -.->|"implements"| ARI
+ ARI --> FRD
+ ARI --> ARD
+ ARI --> SM
+ FRD --> FA
+ ARD --> FAN
+
+ linkStyle default stroke:#D32F2F,stroke-width:2px
+
+ classDef uiNode fill:#4A4A4A,stroke:#D32F2F,color:#FFFFFF
+ classDef domainNode fill:#333333,stroke:#D32F2F,color:#FFFFFF
+ classDef dataNode fill:#333333,stroke:#D32F2F,color:#FFFFFF
+ classDef firebaseNode fill:#4A4A4A,stroke:#D32F2F,color:#FFFFFF
+
+ class LS,LVM uiNode
+ class LU,AR domainNode
+ class ARI,FRD,ARD,SM dataNode
+ class FA,FAN firebaseNode
+
+ style UI fill:#333333,stroke:#D32F2F,color:#FFFFFF
+ style Domain fill:#4A4A4A,stroke:#333333,color:#FFFFFF
+ style Data fill:#D3D3D3,stroke:#D32F2F,color:#000000
+ style Firebase fill:#333333,stroke:#D32F2F,color:#FFFFFF
```
+#### Telemetry and Analytics Integration
+
+```mermaid
+flowchart LR
+ USER["User Login"] --> AUTH["Authentication Success"]
+ AUTH --> ANALYTICS["AnalyticsRemoteDataSource"]
+ ANALYTICS --> FIREBASE["FirebaseAnalytics.logEvent()"]
+ FIREBASE --> DEBUG["Visible in Firebase DebugView"]
+
+ linkStyle default stroke:#D32F2F,stroke-width:2px
+
+ classDef telemetryNode fill:#333333,stroke:#D32F2F,color:#FFFFFF
+
+ class USER,AUTH,ANALYTICS,FIREBASE,DEBUG telemetryNode
+```
+
---
## 4. Technical Implementation Standards
-### 4.1 Reactive Orchestration
-The application uses **Kotlin Coroutines and Flow** for all asynchronous operations.
-- **State-Driven Navigation**: `MainActivity` observes `SessionManager.isAuthenticated`; state changes trigger navigation transitions via `LaunchedEffect`.
-- **Debounced Validation**: Login inputs are validated using a 300ms debounce to optimize performance.
-- **State Pushing**: ViewModels push immutable state objects to the UI, ensuring that recompositions are predictable and efficient.
+Rho Studio UI is engineered for sensitive information (fintech) environments
+
+### 4.1 Session Isolation
+
```mermaid
sequenceDiagram
participant UI as MainActivity
participant SM as SessionManager
- participant Nav as NavController
+ participant CM as ComponentManager
+ participant FC as FeatureComponent
UI->>SM: collectAsState()
- SM-->>UI: AuthState (Unauthenticated)
- UI->>Nav: navigate to Login
-
- Note over UI,Nav: User clicks Login
- UI->>LoginViewModel: onLoginClicked()
- LoginViewModel->>LoginUseCase: login(email, password)
- LoginUseCase->>AuthRepository: login(credentials)
- AuthRepository-->>LoginUseCase: User
- LoginUseCase->>SessionManager: updateSession(user)
-
- SM-->>UI: AuthState (Authenticated)
- UI->>Nav: navigate to Home
+ SM-->>UI: SessionState (Guest)
+ UI->>CM: getAppComponent()
+ CM-->>UI: AppComponent
+
+ Note over UI,FC: User logs in
+ UI->>SM: updateSession(User)
+ SM-->>UI: SessionState (Authenticated)
+ UI->>CM: getUserComponent()
+ CM-->>UI: UserComponent
+
+ Note over UI,FC: User logs out
+ UI->>SM: clearSession()
+ SM-->>UI: SessionState (Guest)
+ UI->>CM: releaseUserComponent()
+ Note over CM: @UserScope objects
binary-purged from memory
```
### 4.2 Modularization Strategy
@@ -237,14 +558,58 @@ Located in `:core:ui`, the design system defines the application's visual langua
---
-## 5. Roadmap & Evolution: Strategic Phases
+## 5. Verification & Quality Assurance
+
+### 5.1 Automated Tests
+
+| Test Suite | Scope | Status |
+| :--- | :--- | :---: |
+| **DI Graph Audit** (`DaggerGraphTest`) | Verifies all components and providers (Firebase, Analytics) are correctly satisfied | Passed |
+| **Transactional Integrity** (`SessionManagerTest`) | Validates atomic state flow and DataStore synchronization | Passed |
+| **Business Logic** (`LoginUseCaseTest`, `LogoutUseCaseTest`) | 90%+ coverage of core transactions using MockK | Passed |
+| **CI/CD Build** | Verified on GitHub Actions including `google-services.json` integration | Passed |
+
+### 5.2 Manual QA Test Plan
+
+**Scenario 1**: Fresh Install / First Launch
+1. Open the app.
+2. Expected: App shows LoadingScreen (CircularProgress), then transitions to Login Screen once session check is complete.
+
+**Scenario 2**: Successful Login & Data Loading
+1. Enter valid Firebase credentials.
+2. Click "**Login**".
+3. Expected:
+ - Circular progress overlay appears.
+ - On success, toast "Login successful!" appears.
+ - UI transitions to Home Screen.
+ - Header displays correct user email/name.
+ - Firebase Analytics event is visible in DebugView.
+
+**Scenario 3**: Secure Logout & Session Isolation
+1. On the Home Screen, click "Logout".
+2. Expected:
+ - UI transitions immediately back to Login Screen.
+ - User input fields in Login are cleared (form reset).
+ - Verification: Using Android Profiler, confirm that @UserScope objects (e.g., HomeViewModel) are cleared from heap.
+
+### 5.3 Regression Checklist for QA
+- Verify that no `UninitializedPropertyAccessException` occurs during rapid Login/Logout cycles.
+- Verify that the `PageHeader` reactively updates when a new user logs in.
+- Confirm that Firebase Analytics events are visible in DebugView.
+- Verify that `@UserScope` objects are destroyed on logout (Android Profiler).
+
+---
+## 6. Roadmap & Evolution: Strategic Phases
The application is transitioning from a modular prototype to a production-hardened system. The evolution is structured into three strategic phases:
-### Phase I: Dependency Orchestration & Decoupling
+### Phase I: Dependency Orchestration & Decoupling - [COMPLETED v1.0.3]
- **Dagger Migration**: Implementation of **Dagger 2** to replace manual Service Locators.
- Define `@Component` and `@Module` boundaries for `:core` and `:features`.
- Implement `@Inject` for UseCase and ViewModel construction to ensure compile-time dependency safety.
+- **ViewModel Multibinding**: Centralized ViewModel registry using `@IntoMap` and `DaggerViewModelFactory`.
+- **Component Dependencies**: Hierarchical component architecture with `CoreComponent`, `AppComponent`, and `UserComponent`.
+- **Session Isolation**: Physical destruction of `@UserScope` graph on logout to prevent data leakage.
- **Interface Segregation**: Strict enforcement of domain-defined interfaces to further isolate the Data Layer from Business Logic.
### Phase II: Transactional Integrity & persistence
@@ -252,9 +617,6 @@ The application is transitioning from a modular prototype to a production-harden
- Implementation of an atomic token refresh mechanism within the Data Layer.
- Securing critical transaction flows by validating session integrity before high-stakes domain executions.
- Complete token lifecycle: Acquisition → Persistence → Validation → Refresh → Recovery → Invalidation.
-- **Offline-First with Room**:
- - Integration of **Room Database** as the local cache for service modules.
- - Implementation of a "Source of Truth" strategy in Repositories to handle network-to-local synchronization.
```mermaid
flowchart TD
A[1. Acquisition
LoginUseCase --> AuthRepository.login]
@@ -270,76 +632,50 @@ flowchart TD
E --> C
F --> H[Reset AuthState]
- style A fill:#e94560,stroke:#c62828,color:#ffffff
- style B fill:#16213e,stroke:#0f3460,color:#ffffff
- style C fill:#1a1a2e,stroke:#e94560,color:#ffffff
- style D fill:#0f3460,stroke:#16213e,color:#ffffff
- style E fill:#16213e,stroke:#0f3460,color:#ffffff
- style F fill:#e94560,stroke:#c62828,color:#ffffff
- style G fill:#0f3460,stroke:#16213e,color:#ffffff
- style H fill:#1a1a2e,stroke:#e94560,color:#ffffff
+ style A fill:#D32F2F,stroke:#FFFFFF,color:#FFFFFF
+ style B fill:#333333,stroke:#D32F2F,color:#FFFFFF
+ style C fill:#4A4A4A,stroke:#333333,color:#FFFFFF
+ style D fill:#333333,stroke:#D32F2F,color:#FFFFFF
+ style E fill:#4A4A4A,stroke:#333333,color:#FFFFFF
+ style F fill:#D32F2F,stroke:#FFFFFF,color:#FFFFFF
+ style G fill:#333333,stroke:#D32F2F,color:#FFFFFF
+ style H fill:#4A4A4A,stroke:#333333,color:#FFFFFF
```
-### Phase III: Verification & Quality Engineering
-- **Domain Test Suite**: Achieving 90%+ coverage for `:core:domain` logic using JUnit 5 and MockK.
-- **UI & Regression Testing**:
- - Implementation of **Compose UI Tests** for critical user journeys (Login, Home navigation).
- - Integration of **Screenshot Testing** to ensure visual consistency across the Rho Studio design system.
-- **Performance Profiling**: Regular benchmarking of recomposition counts and memory allocation in high-density feature screens.
+- **Offline-First with Room**:
+ - Integration of **Room Database** as the local cache for service modules.
+ - Implementation of a "Source of Truth" strategy in Repositories to handle **network-to-local synchronization**.
+
```mermaid
-flowchart LR
- subgraph Current["Current Flow"]
- C1[UI] --> C2[ViewModel] --> C3[UseCase] --> C4[Repository] --> C5[SharedPreferences/Mock Auth]
+flowchart TD
+ subgraph Domain["Domain Layer"]
+ UC["TokenUseCases
- ValidateTokenUseCase
- RefreshTokenUseCase
- RevokeTokenUseCase"]
+ Entities["AuthToken.kt
- accessToken
- refreshToken
- expiresAt"]
+ end
+
+ subgraph Data["Data Layer"]
+ Repo["AuthRepositoryImpl
- refreshToken()
- revokeToken()"]
+ Store["TokenStore
- EncryptedSharedPreferences
- In-memory cache"]
+ SM["SessionManager
- SessionState machine"]
end
- subgraph Planned["Planned Flow"]
- P1[UI] --> P2[ViewModel] --> P3[UseCase] --> P4[Repository]
- P4 --> P5[Local: Room Database]
- P4 --> P6[Remote: Retrofit/Firebase]
+ subgraph Security["Security Layer"]
+ Keystore["Android Keystore
- MasterKey (AES-256-GCM)"]
+ Encrypted["EncryptedSharedPreferences"]
end
- Current -.->|"Evolution"| Planned
+ UC --> Repo
+ Repo --> Store
+ Store --> Encrypted
+ Encrypted --> Keystore
- style Current fill:#1a1a2e,stroke:#e94560,color:#ffffff
- style Planned fill:#0f3460,stroke:#16213e,color:#ffffff
+ style Domain fill:#333333,stroke:#D32F2F,color:#FFFFFF
+ style Data fill:#4A4A4A,stroke:#333333,color:#FFFFFF
+ style Security fill:#D32F2F,stroke:#FFFFFF,color:#FFFFFF
```
----
-## 6. Verification & Quality Assurance
-- **CI/CD**: GitHub Actions pipeline verifies every commit against build and test suites.
-- **Static Analysis**: Automated linting and ASCII metadata headers enforce code style and legal standards.
-
-## 7. File Registry and responsibilities
-Here is the updated table based on the file structure provided:
-
-| File / Module | Layer | Responsibility | Status |
-|:----------------------------------|:-------------|:-------------------------------------|:-------|
-| `MainActivity.kt` | UI Layer | Navigation orchestration | ✅ |
-| `BaseViewModel.kt` | UI Layer | Loading/Error state management | ✅ |
-| `HeaderViewModel.kt` | UI Layer | Session state bridging | ✅ |
-| `PageHeader.kt` / `PageFooter.kt` | UI Layer | Shared UI components | ✅ |
-| `LoginScreen.kt` | UI Layer | Login UI entry point | ✅ |
-| `LoginViewModel.kt` | UI Layer | Form state & validation | ✅ |
-| `LoginEmailField.kt` | UI Layer | Email input with validation | ✅ |
-| `LoginPasswordField.kt` | UI Layer | Password input with security | ✅ |
-| `LoginButton.kt` | UI Layer | Login action button | ✅ |
-| `HomeScreen.kt` | UI Layer | Home UI entry point | ✅ |
-| `HomeViewModel.kt` | UI Layer | Home state & session termination | ✅ |
-| `ServiceList.kt` | UI Layer | Service list grid component | ✅ |
-| `ServiceItem.kt` | UI Layer | Individual service item component | ✅ |
-| `ServiceModule.kt` | UI Layer | Feature-specific model (Home) | ✅ |
-| `BaseUseCase.kt` | Domain Layer | Standardized UseCase abstraction | ✅ |
-| `LoginUseCase.kt` | Domain Layer | Atomic authentication transaction | ✅ |
-| `LogoutUseCase.kt` | Domain Layer | Session teardown orchestration | ✅ |
-| `SessionManagerInterface.kt` | Domain Layer | Session operations contract | ✅ |
-| `AuthRepository.kt` | Domain Layer | Authentication contract | ✅ |
-| `SessionRepository.kt` | Domain Layer | Session persistence contract | ✅ |
-| `User.kt` / `Credentials.kt` | Domain Layer | Pure Kotlin Entities | ✅ |
-| `SessionManager.kt` | Data Layer | SSOT for authentication | ✅ |
-| `AuthRepositoryImpl.kt` | Data Layer | Mock auth (Firebase **planned**) | ⚠️ |
-| `SessionRepositoryImpl.kt` | Data Layer | SharedPreferences (Room **planned**) | ⚠️ |
-| `RefreshTokenUseCase.kt` | Domain Layer | Token refresh (**planned**) | 📅 |
-| `Dagger Components` | App Root | DI setup (**planned**) | 📅 |
+
+
## 8. References & Standards
- **MAD (Modern Android Development)**: Adhering to official [Android Architecture Guidelines](https://developer.android.com/topic/architecture).
- **Jetpack Compose Best Practices**: Following UDF ([Unidirectional Data Flow](https://developer.android.com/develop/ui/compose/architecture#udf)) principles for state management.
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 7c321b8..5a73c28 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -2,6 +2,8 @@ plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.parcelize)
+ alias(libs.plugins.ksp)
+ alias(libs.plugins.googleServices)
}
android {
@@ -37,13 +39,6 @@ android {
}
}
-// Add the new DSL here
-kotlin {
- compilerOptions {
- jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
- }
-}
-
dependencies {
implementation(project(path = ":core:domain"))
implementation(project(path = ":core:data"))
@@ -62,9 +57,20 @@ dependencies {
implementation(libs.androidx.material3)
implementation(libs.androidx.compose.material.icons.extended)
implementation(libs.androidx.navigation.compose)
+
+ // Dagger
+ implementation(libs.dagger)
+ ksp(libs.dagger.compiler)
+
+ // Firebase
+ implementation(platform(libs.firebase.bom))
+ implementation(libs.firebase.auth)
+ implementation(libs.firebase.analytics)
+
implementation(libs.gson)
implementation(libs.material)
testImplementation(libs.junit)
+ testImplementation(libs.mockk)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 509af97..ef03309 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools">
error?.let {
@@ -81,20 +82,42 @@ class MainActivity : ComponentActivity() {
}
}
}
+
+ setContent {
+ UITheme {
+ MainContent()
+ }
+ }
}
@Composable
private fun MainContent() {
val navController = rememberNavController()
- val isSessionChecked by sessionManager.isSessionChecked.collectAsState()
- val isAuthenticated by sessionManager.isAuthenticated.collectAsState()
+
+ /**
+ * # Sealed State Management
+ * Uses a sealed class (SessionState) instead of simple booleans to prevent
+ * illegal UI states and ensure the UI is always a reflection of the session truth.
+ */
+ val sessionState by sessionManager.sessionState.collectAsState()
val isLoading by sessionManager.isLoading.collectAsState()
+ val isSessionChecked = sessionState !is SessionState.Uninitialized && sessionState !is SessionState.Checking
+ val isAuthenticated = sessionState is SessionState.Authenticated
+
if (!isSessionChecked) {
LoadingScreen()
return
}
+ /**
+ * # Reactive ViewModel Provisioning
+ * Utilizes Compose-native viewModel() pattern to avoid race conditions.
+ * LoginViewModel is sourced from the persistent AppScope.
+ */
+ val appFactory = ComponentManager.getAppComponent().viewModelFactory()
+ val loginViewModel: LoginViewModel = androidx.lifecycle.viewmodel.compose.viewModel(factory = appFactory)
+
LaunchedEffect(isAuthenticated) {
if (isAuthenticated) {
navController.navigate("home") {
@@ -102,6 +125,14 @@ class MainActivity : ComponentActivity() {
}
} else {
loginViewModel.resetForm()
+
+ /**
+ * # Secure Session Isolation
+ * Atomically destroys the authenticated dependency graph on logout,
+ * ensuring PII (Personally Identifiable Information) is binary-purged from memory.
+ */
+ ComponentManager.destroyUserComponent()
+
navController.navigate("login") {
popUpTo("home") { inclusive = true }
}
@@ -117,7 +148,19 @@ class MainActivity : ComponentActivity() {
LoginScreen(viewModel = loginViewModel)
}
composable("home") {
- HomeScreen(homeViewModel = homeViewModel)
+ /**
+ * # Tiered DI Scoping
+ * Home and Header ViewModels are provided by the dynamic UserComponent,
+ * which only exists while the user is actively authenticated.
+ */
+ val userFactory = ComponentManager.createUserComponent().viewModelFactory()
+ val homeViewModel: HomeViewModel = androidx.lifecycle.viewmodel.compose.viewModel(factory = userFactory)
+ val headerViewModel: HeaderViewModel = androidx.lifecycle.viewmodel.compose.viewModel(factory = userFactory)
+
+ HomeScreen(
+ homeViewModel = homeViewModel,
+ headerViewModel = headerViewModel
+ )
}
}
diff --git a/app/src/main/java/com/rho/studio/ui/RhoStudioUIApp.kt b/app/src/main/java/com/rho/studio/ui/RhoStudioUIApp.kt
new file mode 100644
index 0000000..3177fba
--- /dev/null
+++ b/app/src/main/java/com/rho/studio/ui/RhoStudioUIApp.kt
@@ -0,0 +1,33 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ==========================================================================
+ * File: RhoStudioUIApp.kt
+ * Author: Alexis Tercero
+ * Email: alexis.tercero@rho.studio
+ * Date: 2026-08-17
+ * ==========================================================================
+ * Description:
+ * Main Application class for RhoStudio UI. Responsible for initializing
+ * core services including Firebase and the dependency injection framework.
+ * ==========================================================================
+ */
+package com.rho.studio.ui
+
+import android.app.Application
+import com.google.firebase.FirebaseApp
+import com.rho.studio.ui.di.ComponentManager
+
+class RhoStudioUIApp : Application() {
+
+ override fun onCreate() {
+ super.onCreate()
+ FirebaseApp.initializeApp(this)
+ ComponentManager.init(this)
+ }
+}
diff --git a/app/src/main/java/com/rho/studio/ui/di/AppComponent.kt b/app/src/main/java/com/rho/studio/ui/di/AppComponent.kt
new file mode 100644
index 0000000..b1eb9ef
--- /dev/null
+++ b/app/src/main/java/com/rho/studio/ui/di/AppComponent.kt
@@ -0,0 +1,45 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ==============================================================================================
+ * File: AppComponent.kt
+ * Author: Alexis Tercero
+ * Email: alexis.tercero@rho.studio
+ * Date: 2026-08-14
+ * ==============================================================================================
+ * Description: Root Dagger component for the application.
+ * Responsible for bridging the core data layers with the UI layer and
+ * managing the application-wide dependency graph.
+ * ==============================================================================================
+ */
+package com.rho.studio.ui.di
+
+import com.rho.studio.ui.MainActivity
+import com.rho.studio.ui.core.data.di.CoreComponent
+import com.rho.studio.ui.core.ui.di.DaggerViewModelFactory
+import com.rho.studio.ui.features.auth.di.AuthModule
+import dagger.Component
+import javax.inject.Scope
+
+@Scope
+@Retention(AnnotationRetention.RUNTIME)
+annotation class AppScope
+
+@AppScope
+@Component(
+ dependencies = [CoreComponent::class],
+ modules = [AuthModule::class, com.rho.studio.ui.core.ui.di.UIModule::class]
+)
+interface AppComponent {
+ fun inject(activity: MainActivity)
+
+ // Exposed for UserComponent
+ fun coreComponent(): CoreComponent
+
+ fun viewModelFactory(): DaggerViewModelFactory
+}
diff --git a/app/src/main/java/com/rho/studio/ui/di/ComponentManager.kt b/app/src/main/java/com/rho/studio/ui/di/ComponentManager.kt
new file mode 100644
index 0000000..5ffa139
--- /dev/null
+++ b/app/src/main/java/com/rho/studio/ui/di/ComponentManager.kt
@@ -0,0 +1,60 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ==============================================================================================
+ * File: ComponentManager.kt
+ * Author: Alexis Tercero
+ * Email: alexis.tercero@rho.studio
+ * Date: 2026-08-14
+ * ==============================================================================================
+ * Description: Centralized manager for the Dagger component hierarchy.
+ * Handles the lifecycle of global (App) and scoped (User) components.
+ * Enables session-based dependency injection by providing mechanisms to
+ * initialize and tear down the UserComponent upon login/logout.
+ * ==============================================================================================
+ */
+package com.rho.studio.ui.di
+
+import android.content.Context
+import com.rho.studio.ui.core.data.di.CoreComponent
+import com.rho.studio.ui.core.data.di.CoreModule
+import com.rho.studio.ui.core.data.di.DaggerCoreComponent
+
+object ComponentManager {
+
+ private lateinit var coreComponent: CoreComponent
+ private lateinit var appComponent: AppComponent
+ private var userComponent: UserComponent? = null
+
+ fun init(context: Context) {
+ CoreModule.init(context)
+ coreComponent = DaggerCoreComponent.builder()
+ .build()
+
+ appComponent = DaggerAppComponent.builder()
+ .coreComponent(coreComponent)
+ .build()
+ }
+
+ fun getAppComponent(): AppComponent = appComponent
+
+ fun createUserComponent(): UserComponent {
+ if (userComponent == null) {
+ userComponent = DaggerUserComponent.builder()
+ .coreComponent(coreComponent)
+ .build()
+ }
+ return userComponent!!
+ }
+
+ fun destroyUserComponent() {
+ userComponent = null
+ }
+
+ fun getUserComponent(): UserComponent? = userComponent
+}
diff --git a/app/src/main/java/com/rho/studio/ui/di/UserComponent.kt b/app/src/main/java/com/rho/studio/ui/di/UserComponent.kt
new file mode 100644
index 0000000..aab6d48
--- /dev/null
+++ b/app/src/main/java/com/rho/studio/ui/di/UserComponent.kt
@@ -0,0 +1,40 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ==============================================================================================
+ * File: UserComponent.kt
+ * Author: Alexis Tercero
+ * Email: alexis.tercero@rho.studio
+ * Date: 2026-08-12
+ * ==============================================================================================
+ * Description: Dagger component defining the scope for authenticated user sessions.
+ *
+ * This component acts as the Single Source of Truth (SSOT) for the user's session
+ * lifecycle, managing the injection of dependencies that require a valid user
+ * context. It bridges core data layers with feature-specific modules, ensuring
+ * that sensitive user data is scoped correctly and cleared upon logout.
+ *
+ * Scope: @UserScope
+ * ==============================================================================================
+ */
+package com.rho.studio.ui.di
+
+import com.rho.studio.ui.core.data.di.CoreComponent
+import com.rho.studio.ui.core.ui.di.DaggerViewModelFactory
+import com.rho.studio.ui.core.ui.di.UserScope
+import com.rho.studio.ui.features.home.di.HomeModule
+import dagger.Component
+
+@UserScope
+@Component(
+ dependencies = [CoreComponent::class],
+ modules = [HomeModule::class, com.rho.studio.ui.core.ui.di.UIModule::class]
+)
+interface UserComponent {
+ fun viewModelFactory(): DaggerViewModelFactory
+}
diff --git a/app/src/test/java/com/rho/studio/ui/di/DaggerGraphTest.kt b/app/src/test/java/com/rho/studio/ui/di/DaggerGraphTest.kt
new file mode 100644
index 0000000..01fbd37
--- /dev/null
+++ b/app/src/test/java/com/rho/studio/ui/di/DaggerGraphTest.kt
@@ -0,0 +1,73 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ==========================================================================
+ * File: DaggerGraphTest.kt
+ * Author: Alexis Tercero
+ * Email: alexis.tercero@rho.studio
+ * Date: 2026-08-25
+ * ==========================================================================
+ * Description: Test suite for Dagger graph integrity.
+ * Verifies that all components and modules are correctly wired
+ * and that the scoped lifecycle of components is maintained.
+ * ==========================================================================
+ */
+package com.rho.studio.ui.di
+
+import android.content.Context
+import com.google.firebase.analytics.FirebaseAnalytics
+import com.google.firebase.auth.FirebaseAuth
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import org.junit.Assert.assertNotNull
+import org.junit.Before
+import org.junit.Test
+
+class DaggerGraphTest {
+
+ @Before
+ fun setUp() {
+ mockkStatic(FirebaseAuth::class)
+ every { FirebaseAuth.getInstance() } returns mockk(relaxed = true)
+
+ mockkStatic(FirebaseAnalytics::class)
+ every { FirebaseAnalytics.getInstance(any()) } returns mockk(relaxed = true)
+ }
+
+ @Test
+ fun `verify Dagger graph initialization`() {
+ val mockContext = mockk(relaxed = true)
+
+ // Initialize ComponentManager
+ ComponentManager.init(mockContext)
+
+ val appComponent = ComponentManager.getAppComponent()
+ assertNotNull(appComponent)
+ assertNotNull(appComponent.viewModelFactory())
+
+ // Verify CoreComponent exposures
+ val coreComponent = appComponent.coreComponent()
+ assertNotNull(coreComponent)
+ assertNotNull(coreComponent.sessionManager())
+ assertNotNull(coreComponent.authRepository())
+ }
+
+ @Test
+ fun `verify UserComponent lifecycle`() {
+ val mockContext = mockk(relaxed = true)
+ ComponentManager.init(mockContext)
+
+ val userComponent = ComponentManager.createUserComponent()
+ assertNotNull(userComponent)
+ assertNotNull(userComponent.viewModelFactory())
+
+ ComponentManager.destroyUserComponent()
+ // Note: ComponentManager.getUserComponent() would return null now
+ }
+}
diff --git a/build.gradle.kts b/build.gradle.kts
index 8547f7a..cffffba 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -3,4 +3,6 @@ plugins {
alias(libs.plugins.android.library) apply false
alias(libs.plugins.kotlin.compose) apply false
alias(libs.plugins.kotlin.parcelize) apply false
+ alias(libs.plugins.ksp) apply false
+ alias(libs.plugins.googleServices) apply false
}
\ No newline at end of file
diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts
index 9dc8ef2..44b0e18 100644
--- a/core/data/build.gradle.kts
+++ b/core/data/build.gradle.kts
@@ -1,5 +1,6 @@
plugins {
alias(libs.plugins.android.library)
+ alias(libs.plugins.ksp)
}
android {
@@ -19,7 +20,25 @@ android {
dependencies {
implementation(project(path = ":core:domain"))
+
+ // Dagger
+ implementation(libs.dagger)
+ ksp(libs.dagger.compiler)
+
+ // DataStore
+ implementation(libs.datastore.preferences)
+
+ // Firebase
+ implementation(platform(libs.firebase.bom))
+ implementation(libs.firebase.auth)
+ implementation(libs.firebase.analytics)
+ implementation(libs.kotlinx.coroutines.play.services)
+
implementation(libs.androidx.core.ktx)
implementation(libs.gson)
implementation(libs.androidx.lifecycle.runtime.ktx)
+
+ testImplementation(libs.junit)
+ testImplementation(libs.mockk)
+ testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
}
diff --git a/core/data/src/main/java/com/rho/studio/ui/core/data/di/CoreComponent.kt b/core/data/src/main/java/com/rho/studio/ui/core/data/di/CoreComponent.kt
new file mode 100644
index 0000000..9b2014a
--- /dev/null
+++ b/core/data/src/main/java/com/rho/studio/ui/core/data/di/CoreComponent.kt
@@ -0,0 +1,38 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ==============================================================================================
+ * File: CoreComponent.kt
+ * Author: Alexis Tercero
+ * Email: alexis.tercero@rho.studio
+ * Date: 2026-08-12
+ * ==============================================================================================
+ * Description: Dagger component responsible for providing core infrastructure dependencies,
+ * including session management, authentication repositories, and application context.
+ * ==============================================================================================
+ */
+package com.rho.studio.ui.core.data.di
+
+import android.content.Context
+import com.rho.studio.ui.core.data.manager.SessionManager
+import com.rho.studio.ui.core.domain.repository.SessionRepository
+import com.rho.studio.ui.core.domain.repository.AuthRepository
+import com.rho.studio.ui.core.domain.usecase.SessionManagerInterface
+import dagger.Component
+import javax.inject.Singleton
+
+@Singleton
+@Component(modules = [CoreModule::class])
+interface CoreComponent {
+
+ fun context(): Context
+ fun sessionRepository(): SessionRepository
+ fun sessionManager(): SessionManager
+ fun sessionManagerInterface(): SessionManagerInterface
+ fun authRepository(): AuthRepository
+}
diff --git a/core/data/src/main/java/com/rho/studio/ui/core/data/di/CoreModule.kt b/core/data/src/main/java/com/rho/studio/ui/core/data/di/CoreModule.kt
new file mode 100644
index 0000000..f4a5ae7
--- /dev/null
+++ b/core/data/src/main/java/com/rho/studio/ui/core/data/di/CoreModule.kt
@@ -0,0 +1,87 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ==============================================================================================
+ * File: CoreModule.kt
+ * Author: Alexis Tercero
+ * Email: alexis.tercero@rho.studio
+ * Date: 2026-08-14
+ * ==============================================================================================
+ * Description: Dagger module responsible for providing core infrastructure dependencies.
+ * It centralizes the injection of data sources, repositories, and cross-cutting
+ * concerns like threading and analytics.
+ *
+ * Responsibilities:
+ * - Binding implementations to domain-level Repository interfaces (Auth and Session).
+ * - Providing Singleton instances of Firebase services (Auth, Analytics).
+ * - Managing global application context for core-level dependencies.
+ * - Defining standard CoroutineDispatchers for background operations.
+ * ==============================================================================================
+ */
+package com.rho.studio.ui.core.data.di
+
+import android.Manifest
+import android.content.Context
+import androidx.annotation.RequiresPermission
+import com.rho.studio.ui.core.data.manager.SessionManager
+import com.rho.studio.ui.core.data.repository.SessionRepositoryImpl
+import com.rho.studio.ui.core.data.repository.AuthRepositoryImpl
+import com.rho.studio.ui.core.domain.repository.SessionRepository
+import com.rho.studio.ui.core.domain.repository.AuthRepository
+import com.rho.studio.ui.core.domain.usecase.SessionManagerInterface
+import com.google.firebase.auth.FirebaseAuth
+import com.google.firebase.analytics.FirebaseAnalytics
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import javax.inject.Singleton
+
+@Module
+abstract class CoreModule {
+
+ @Binds
+ @Singleton
+ abstract fun bindSessionRepository(impl: SessionRepositoryImpl): SessionRepository
+
+ @Binds
+ @Singleton
+ abstract fun bindAuthRepository(impl: AuthRepositoryImpl): AuthRepository
+
+ @Binds
+ @Singleton
+ abstract fun bindSessionManagerInterface(impl: SessionManager): SessionManagerInterface
+
+ companion object {
+ private var appContext: Context? = null
+
+ fun init(context: Context) {
+ appContext = context.applicationContext
+ }
+
+ @Provides
+ @Singleton
+ fun provideContext(): Context {
+ return appContext ?: throw IllegalStateException("CoreModule must be initialized with init(context)")
+ }
+
+ @Provides
+ @Singleton
+ fun provideFirebaseAuth(): FirebaseAuth = FirebaseAuth.getInstance()
+
+ @RequiresPermission(allOf = [Manifest.permission.INTERNET, Manifest.permission.ACCESS_NETWORK_STATE, Manifest.permission.WAKE_LOCK])
+ @Provides
+ @Singleton
+ fun provideFirebaseAnalytics(context: Context): FirebaseAnalytics = FirebaseAnalytics.getInstance(context)
+
+ @Provides
+ @Singleton
+ fun provideIODispatcher(): CoroutineDispatcher = Dispatchers.IO
+ }
+}
diff --git a/core/data/src/main/java/com/rho/studio/ui/core/data/manager/SessionManager.kt b/core/data/src/main/java/com/rho/studio/ui/core/data/manager/SessionManager.kt
index bfdde4c..46f7551 100644
--- a/core/data/src/main/java/com/rho/studio/ui/core/data/manager/SessionManager.kt
+++ b/core/data/src/main/java/com/rho/studio/ui/core/data/manager/SessionManager.kt
@@ -10,110 +10,57 @@
* File: SessionManager.kt
* Author: Alexis Tercero
* Email: alexis.tercero@rho.studio
- * Date: 2026-08-04
+ * Date: 2026-08-12
* ==============================================================================================
- * Description: Singleton orchestrator for authentication state and session lifecycle.
- * Delegates persistence to a SessionRepository.kt and exposes reactive state
- * via Kotlin Coroutines StateFlow for lifecycle-aware UI updates.
+ * Description: Orchestrator for authentication state and session lifecycle.
+ * Acts as the Single Source of Truth (SSOT) for the user's session.
* ==============================================================================================
*/
package com.rho.studio.ui.core.data.manager
-import android.content.Context
+import com.rho.studio.ui.core.domain.model.SessionState
import com.rho.studio.ui.core.domain.model.User
-import com.rho.studio.ui.core.data.repository.SessionRepository
-import com.rho.studio.ui.core.data.repository.SessionRepositoryImpl
+import com.rho.studio.ui.core.domain.repository.SessionRepository
import com.rho.studio.ui.core.domain.usecase.SessionManagerInterface
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
+import javax.inject.Inject
+import javax.inject.Singleton
/**
* # SessionManager
* Implementation of SessionManagerInterface and SSOT for session state.
*/
-class SessionManager private constructor() : SessionManagerInterface {
+@Singleton
+class SessionManager @Inject constructor(
+ private val repository: SessionRepository,
+ private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
+) : SessionManagerInterface {
- companion object {
+ private val sessionScope = CoroutineScope(ioDispatcher + SupervisorJob())
- /** ensure that changes to the instance variable in a SessionManager are
- * immediately visible to all threads, preventing issues caused
- * by thread-local caching.*/
- @Volatile
- private var instance: SessionManager? = null
+ private val _sessionState = MutableStateFlow(SessionState.Uninitialized)
+ val sessionState: StateFlow = _sessionState.asStateFlow()
- /** Thread-safe singleton instance getter.*/
- fun getInstance(): SessionManager {
- return instance ?: synchronized(this) {
- instance ?: SessionManager().also { instance = it }
- }
- }
-
- /**
- * Initialize the session manager with application context.
- * Should be called in Application.onCreate() or MainActivity.onCreate().
- */
- fun init(context: Context) {
- getInstance().initialize(SessionRepositoryImpl(context))
- }
-
- fun init(repository: SessionRepository) {
- getInstance().initialize(repository)
- }
- }
-
- // ==================== PROPERTIES ====================
-
- /**Instead of blocking the UI thread when saving
- * to disk or simulating a network call,
- * it uses a dedicated sessionScope*/
- private val sessionScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
- private lateinit var repository: SessionRepository
- private var isInitialized = false
-
- /** # Private MutableStateFlow
- * Only the SessionManager can modify the state*/
- private val _isAuthenticated = MutableStateFlow(false)
- private val _currentUser = MutableStateFlow(null)
private val _isLoading = MutableStateFlow(false)
- private val _error = MutableStateFlow(null)
- private val _isSessionChecked = MutableStateFlow(false)
-
- /** # PUBLIC StateFlow
- * The rest of the app (UI/ViewModels) can only observe the state.*/
- val isAuthenticated: StateFlow = _isAuthenticated.asStateFlow()
- val currentUser: StateFlow = _currentUser.asStateFlow()
val isLoading: StateFlow = _isLoading.asStateFlow()
- val error: StateFlow = _error.asStateFlow()
- val isSessionChecked: StateFlow = _isSessionChecked.asStateFlow()
- // ==================== INITIALIZATION ====================
- private fun initialize(repository: SessionRepository) {
- if (isInitialized) return
+ private val _error = MutableStateFlow(null)
+ val error: StateFlow = _error.asStateFlow()
- this.repository = repository
- isInitialized = true
+ init {
loadSavedSession()
}
- private fun checkInitialized() {
- if (!isInitialized) {
- throw IllegalStateException("SessionManager must be initialized with init(context) before use.")
- }
- }
-
override fun updateSession(user: User) {
- checkInitialized()
- _currentUser.value = user
- _isAuthenticated.value = true
+ _sessionState.value = SessionState.Authenticated(user)
saveUserSession(user)
}
override fun clearSession() {
- checkInitialized()
- _currentUser.value = null
- _isAuthenticated.value = false
+ _sessionState.value = SessionState.Guest
clearUserSession()
}
@@ -124,20 +71,18 @@ class SessionManager private constructor() : SessionManagerInterface {
}
private fun loadSavedSession() {
+ _sessionState.value = SessionState.Checking
sessionScope.launch {
try {
- val user = repository.getUser() // persistence
+ val user = repository.getUser()
if (user != null) {
- _currentUser.value = user
- _isAuthenticated.value = true
+ _sessionState.value = SessionState.Authenticated(user)
} else {
- _isAuthenticated.value = false
+ _sessionState.value = SessionState.Guest
}
} catch (e: Exception) {
- _isAuthenticated.value = false
+ _sessionState.value = SessionState.Guest
clearUserSession()
- } finally {
- _isSessionChecked.value = true
}
}
}
@@ -147,6 +92,7 @@ class SessionManager private constructor() : SessionManagerInterface {
try {
repository.saveUser(user)
} catch (e: Exception) {
+ // Log error
}
}
}
@@ -156,12 +102,20 @@ class SessionManager private constructor() : SessionManagerInterface {
try {
repository.clearSession()
} catch (e: Exception) {
+ // Log error
}
}
}
- fun isAuthenticatedSync(): Boolean = _isAuthenticated.value
- fun getCurrentUserSync(): User? = _currentUser.value
- fun clearError() { _error.value = null }
- fun cleanup() { sessionScope.cancel() }
+ fun isAuthenticated(): Boolean = _sessionState.value is SessionState.Authenticated
+ fun getCurrentUser(): User? = (_sessionState.value as? SessionState.Authenticated)?.user
+ fun isSessionChecked(): Boolean = _sessionState.value !is SessionState.Uninitialized && _sessionState.value !is SessionState.Checking
+
+ fun clearError() {
+ _error.value = null
+ }
+
+ fun cleanup() {
+ sessionScope.cancel()
+ }
}
diff --git a/core/data/src/main/java/com/rho/studio/ui/core/data/remote/AnalyticsRemoteDataSource.kt b/core/data/src/main/java/com/rho/studio/ui/core/data/remote/AnalyticsRemoteDataSource.kt
new file mode 100644
index 0000000..a4d0659
--- /dev/null
+++ b/core/data/src/main/java/com/rho/studio/ui/core/data/remote/AnalyticsRemoteDataSource.kt
@@ -0,0 +1,43 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ==============================================================================================
+ * File: AnalyticsRemoteDataSource.kt
+ * Author: Alexis Tercero
+ * Email: alexis.tercero@rho.studio
+ * Date: 2026-08-13
+ * ==============================================================================================
+ * Description: Firebase analytics data layer for decoupled event logging.
+ * ==============================================================================================
+ */
+package com.rho.studio.ui.core.data.remote
+
+import android.os.Bundle
+import android.util.Log
+import com.google.firebase.analytics.FirebaseAnalytics
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * Wrapper around FirebaseAnalytics for decoupled event logging.
+ */
+@Singleton
+class AnalyticsRemoteDataSource @Inject constructor(
+ private val firebaseAnalytics: FirebaseAnalytics
+) {
+ fun logLogin(userId: String) {
+ Log.d("RHO_TELEMETRY", "Logging login event for user: $userId")
+ // Set the user identity for all future events in the session
+ firebaseAnalytics.setUserId(userId)
+
+ val bundle = Bundle().apply {
+ putString(FirebaseAnalytics.Param.METHOD, "email_password")
+ }
+ firebaseAnalytics.logEvent(FirebaseAnalytics.Event.LOGIN, bundle)
+ }
+}
diff --git a/core/data/src/main/java/com/rho/studio/ui/core/data/remote/FirebaseRemoteDataSource.kt b/core/data/src/main/java/com/rho/studio/ui/core/data/remote/FirebaseRemoteDataSource.kt
new file mode 100644
index 0000000..5ddcc4d
--- /dev/null
+++ b/core/data/src/main/java/com/rho/studio/ui/core/data/remote/FirebaseRemoteDataSource.kt
@@ -0,0 +1,57 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ==============================================================================================
+ * File: FirebaseRemoteDataSource.kt
+ * Author: Alexis Tercero
+ * Email: alexis.tercero@rho.studio
+ * Date: 2026-08-13
+ * ==============================================================================================
+ * Description: Firebase remote data source.
+ * ==============================================================================================
+ */
+package com.rho.studio.ui.core.data.remote
+
+import com.google.firebase.auth.FirebaseAuth
+import com.rho.studio.ui.core.domain.model.Credentials
+import com.rho.studio.ui.core.domain.model.User
+import kotlinx.coroutines.tasks.await
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * Thread-safe wrapper around FirebaseAuth SDK.
+ */
+@Singleton
+class FirebaseRemoteDataSource @Inject constructor(
+ private val firebaseAuth: FirebaseAuth,
+ private val analyticsDataSource: AnalyticsRemoteDataSource
+) {
+ suspend fun login(credentials: Credentials): User {
+ val result = firebaseAuth.signInWithEmailAndPassword(
+ credentials.email,
+ credentials.password
+ ).await()
+
+ val firebaseUser = result.user ?: throw Exception("Firebase user is null")
+
+ analyticsDataSource.logLogin(firebaseUser.uid)
+
+ return User(
+ id = firebaseUser.uid,
+ email = firebaseUser.email ?: credentials.email,
+ name = firebaseUser.displayName ?: extractNameFromEmail(firebaseUser.email ?: credentials.email)
+ )
+ }
+
+ private fun extractNameFromEmail(email: String): String {
+ return email.substringBefore("@")
+ .split(".", "_", "-")
+ .joinToString(" ") { it.replaceFirstChar { c -> c.uppercase() } }
+ }
+}
diff --git a/core/data/src/main/java/com/rho/studio/ui/core/data/repository/AuthRepositoryImpl.kt b/core/data/src/main/java/com/rho/studio/ui/core/data/repository/AuthRepositoryImpl.kt
index 3d891da..2df6248 100644
--- a/core/data/src/main/java/com/rho/studio/ui/core/data/repository/AuthRepositoryImpl.kt
+++ b/core/data/src/main/java/com/rho/studio/ui/core/data/repository/AuthRepositoryImpl.kt
@@ -10,9 +10,9 @@
* File: AuthRepositoryImpl.kt
* Author: Alexis Tercero
* Email: alexis.tercero@rho.studio
- * Date: 2026-08-04
+ * Date: 2026-08-14
* ==============================================================================================
- * Description: Repository for persisting session data. Current as a mock.
+ * Description: Repository for persisting session data.
* ==============================================================================================
*/
package com.rho.studio.ui.core.data.repository
@@ -20,33 +20,18 @@ package com.rho.studio.ui.core.data.repository
import com.rho.studio.ui.core.domain.model.Credentials
import com.rho.studio.ui.core.domain.model.User
import com.rho.studio.ui.core.domain.repository.AuthRepository
-import kotlinx.coroutines.delay
+import com.rho.studio.ui.core.data.remote.FirebaseRemoteDataSource
+import javax.inject.Inject
+import javax.inject.Singleton
/**
- * Mock implementation of [AuthRepository] for development purposes.
- * This will be replaced by Firebase Auth in the future.
+ * Production implementation of [AuthRepository] using Firebase Authentication.
*/
-class AuthRepositoryImpl : AuthRepository {
+@Singleton
+class AuthRepositoryImpl @Inject constructor(
+ private val firebaseDataSource: FirebaseRemoteDataSource
+) : AuthRepository {
override suspend fun login(credentials: Credentials): User {
- // Simulated network delay
- delay(1500)
-
- // Mocking authentication check
- if (credentials.email == "error@rho.studio") {
- throw RuntimeException("Network error")
- }
-
- // Mock User creation
- return User(
- id = "user_${System.currentTimeMillis()}",
- email = credentials.email,
- name = extractNameFromEmail(credentials.email)
- )
- }
-
- private fun extractNameFromEmail(email: String): String {
- return email.substringBefore("@")
- .split(".", "_", "-")
- .joinToString(" ") { it.replaceFirstChar { c -> c.uppercase() } }
+ return firebaseDataSource.login(credentials)
}
}
diff --git a/core/data/src/main/java/com/rho/studio/ui/core/data/repository/SessionRepository.kt b/core/data/src/main/java/com/rho/studio/ui/core/data/repository/SessionRepositoryImpl.kt
similarity index 62%
rename from core/data/src/main/java/com/rho/studio/ui/core/data/repository/SessionRepository.kt
rename to core/data/src/main/java/com/rho/studio/ui/core/data/repository/SessionRepositoryImpl.kt
index e774881..aebcc20 100644
--- a/core/data/src/main/java/com/rho/studio/ui/core/data/repository/SessionRepository.kt
+++ b/core/data/src/main/java/com/rho/studio/ui/core/data/repository/SessionRepositoryImpl.kt
@@ -7,55 +7,55 @@
* ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
*
* ==============================================================================================
- * File: SessionRepository.kt
+ * File: SessionRepositoryImpl.kt
* Author: Alexis Tercero
* Email: alexis.tercero@rho.studio
- * Date: 2026-08-04
+ * Date: 2026-08-12
* ==============================================================================================
- * Description: Repository for persisting session data.
+ * Description: Implementation of SessionRepository using Jetpack DataStore.
* ==============================================================================================
*/
package com.rho.studio.ui.core.data.repository
import android.content.Context
-import android.content.SharedPreferences
-import androidx.core.content.edit
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.stringPreferencesKey
+import androidx.datastore.preferences.preferencesDataStore
import com.google.gson.Gson
import com.rho.studio.ui.core.domain.model.User
+import com.rho.studio.ui.core.domain.repository.SessionRepository
+import kotlinx.coroutines.flow.first
+import javax.inject.Inject
+import javax.inject.Singleton
-/**
- * Interface defining the persistence operations for user sessions.
- */
-interface SessionRepository {
- suspend fun saveUser(user: User)
- suspend fun getUser(): User?
- suspend fun clearSession()
-}
+private val Context.dataStore: DataStore by preferencesDataStore(name = "session_prefs")
/**
- * Implementation of [SessionRepository] using SharedPreferences.
+ * Implementation of [SessionRepository] using Preferences DataStore.
*/
-class SessionRepositoryImpl(context: Context) : SessionRepository {
+@Singleton
+class SessionRepositoryImpl @Inject constructor(
+ private val context: Context
+) : SessionRepository {
- private val preferences: SharedPreferences =
- context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
-
private val gson = Gson()
companion object {
- private const val PREFS_NAME = "session_prefs"
- private const val PREF_KEY_USER = "pref_current_user"
+ private val PREF_KEY_USER = stringPreferencesKey("pref_current_user")
}
override suspend fun saveUser(user: User) {
val userJson = gson.toJson(user)
- preferences.edit {
- putString(PREF_KEY_USER, userJson)
+ context.dataStore.edit { preferences ->
+ preferences[PREF_KEY_USER] = userJson
}
}
override suspend fun getUser(): User? {
- val userJson = preferences.getString(PREF_KEY_USER, null) ?: return null
+ val preferences = context.dataStore.data.first()
+ val userJson = preferences[PREF_KEY_USER] ?: return null
return try {
gson.fromJson(userJson, User::class.java)
} catch (e: Exception) {
@@ -64,8 +64,8 @@ class SessionRepositoryImpl(context: Context) : SessionRepository {
}
override suspend fun clearSession() {
- preferences.edit {
- remove(PREF_KEY_USER)
+ context.dataStore.edit { preferences ->
+ preferences.remove(PREF_KEY_USER)
}
}
}
diff --git a/core/data/src/test/java/com/rho/studio/ui/core/data/manager/SessionManagerTest.kt b/core/data/src/test/java/com/rho/studio/ui/core/data/manager/SessionManagerTest.kt
new file mode 100644
index 0000000..141da9b
--- /dev/null
+++ b/core/data/src/test/java/com/rho/studio/ui/core/data/manager/SessionManagerTest.kt
@@ -0,0 +1,105 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ==========================================================================
+ * File: SessionManagerTest.kt
+ * Author: Alexis Tercero
+ * Email: alexis.tercero@rho.studio
+ * Date: 2026-08-25
+ * ==========================================================================
+ * Description: Test suite for SessionManager.
+ * Verifies the reactive session state machine, initialization logic,
+ * and synchronization with the persistent repository.
+ * ==========================================================================
+ */
+package com.rho.studio.ui.core.data.manager
+
+import com.rho.studio.ui.core.domain.model.SessionState
+import com.rho.studio.ui.core.domain.model.User
+import com.rho.studio.ui.core.domain.repository.SessionRepository
+import io.mockk.*
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.*
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+
+@ExperimentalCoroutinesApi
+class SessionManagerTest {
+
+ private val testDispatcher = StandardTestDispatcher()
+ private lateinit var repository: SessionRepository
+ private lateinit var sessionManager: SessionManager
+
+ @Before
+ fun setUp() {
+ Dispatchers.setMain(testDispatcher)
+ repository = mockk(relaxed = true)
+ }
+
+ @After
+ fun tearDown() {
+ Dispatchers.resetMain()
+ }
+
+ @Test
+ fun `init loads saved session - authenticated`() = runTest {
+ val user = User("1", "test@rho.studio", "Test User")
+ coEvery { repository.getUser() } returns user
+
+ sessionManager = SessionManager(repository, testDispatcher)
+ testDispatcher.scheduler.advanceUntilIdle()
+
+ val state = sessionManager.sessionState.value
+ assertTrue(state is SessionState.Authenticated)
+ assertEquals(user, (state as SessionState.Authenticated).user)
+ }
+
+ @Test
+ fun `init loads saved session - guest`() = runTest {
+ coEvery { repository.getUser() } returns null
+
+ sessionManager = SessionManager(repository, testDispatcher)
+ testDispatcher.scheduler.advanceUntilIdle()
+
+ assertTrue(sessionManager.sessionState.value is SessionState.Guest)
+ }
+
+ @Test
+ fun `updateSession updates state and saves to repository`() = runTest {
+ coEvery { repository.getUser() } returns null
+ sessionManager = SessionManager(repository, testDispatcher)
+ testDispatcher.scheduler.advanceUntilIdle()
+
+ val user = User("2", "new@rho.studio", "New User")
+ sessionManager.updateSession(user)
+ testDispatcher.scheduler.advanceUntilIdle()
+
+ val state = sessionManager.sessionState.value
+ assertTrue(state is SessionState.Authenticated)
+ assertEquals(user, (state as SessionState.Authenticated).user)
+ coVerify { repository.saveUser(user) }
+ }
+
+ @Test
+ fun `clearSession updates state to Guest and clears repository`() = runTest {
+ val user = User("1", "test@rho.studio", "Test User")
+ coEvery { repository.getUser() } returns user
+ sessionManager = SessionManager(repository, testDispatcher)
+ testDispatcher.scheduler.advanceUntilIdle()
+
+ sessionManager.clearSession()
+ testDispatcher.scheduler.advanceUntilIdle()
+
+ assertTrue(sessionManager.sessionState.value is SessionState.Guest)
+ coVerify { repository.clearSession() }
+ }
+}
diff --git a/core/domain/build.gradle.kts b/core/domain/build.gradle.kts
index 743a784..75291b9 100644
--- a/core/domain/build.gradle.kts
+++ b/core/domain/build.gradle.kts
@@ -5,6 +5,8 @@ plugins {
dependencies {
implementation(libs.gson)
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
+ implementation("javax.inject:javax.inject:1")
testImplementation(libs.junit)
+ testImplementation(libs.mockk)
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
}
diff --git a/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/model/Credentials.kt b/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/model/Credentials.kt
index b7e81a2..bee33f1 100644
--- a/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/model/Credentials.kt
+++ b/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/model/Credentials.kt
@@ -10,36 +10,18 @@
* File: Credentials.kt
* Author: Alexis Tercero
* Email: alexis.tercero@rho.studio
- * Date: 2026-08-05
+ * Date: 2026-08-12
* ==============================================================================================
* Description: core:domain
- * Represents the user's authentication data and provides validation logic.
+ * Represents the user's authentication data as standard String values.
* ==============================================================================================
*/
package com.rho.studio.ui.core.domain.model
/**
- * Represents the user's authentication data and provides validation logic.
+ * Represents the user's authentication data.
*/
data class Credentials(
- var email: String = "",
- var password: String = ""
-) {
- companion object {
- private val EMAIL_REGEX = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$".toRegex()
- }
-
- val isEmailValid: Boolean
- get() = email.isNotBlank() && email.matches(EMAIL_REGEX)
-
- val isPasswordValid: Boolean
- get() = password.length >= 6
-
- val isValid: Boolean
- get() = isEmailValid && isPasswordValid
-
- fun clear() {
- email = ""
- password = ""
- }
-}
+ val email: String,
+ val password: String
+)
diff --git a/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/model/SessionState.kt b/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/model/SessionState.kt
new file mode 100644
index 0000000..ebc99bb
--- /dev/null
+++ b/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/model/SessionState.kt
@@ -0,0 +1,31 @@
+/**
+ * Rho Studio®
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ==========================================================================
+ * File: SessionState.kt
+ * Author: Alexis Tercero
+ * Email: alexis.tercero@rho.studio
+ * Date: 2026-08-18
+ * ==========================================================================
+ * Description:
+ * Defines the states of a user session,
+ * to manage authentication and guest access.
+ * ==========================================================================
+ */
+package com.rho.studio.ui.core.domain.model
+
+/**
+ * Sealed class representing the possible states of a user session.
+ */
+sealed class SessionState {
+ object Uninitialized : SessionState()
+ object Checking : SessionState()
+ data class Authenticated(val user: User) : SessionState()
+ object Guest : SessionState()
+}
diff --git a/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/repository/SessionRepository.kt b/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/repository/SessionRepository.kt
new file mode 100644
index 0000000..2ebc3da
--- /dev/null
+++ b/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/repository/SessionRepository.kt
@@ -0,0 +1,29 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ==============================================================================================
+ * File: SessionRepository.kt
+ * Author: Alexis Tercero
+ * Email: alexis.tercero@rho.studio
+ * Date: 2026-08-12
+ * ==============================================================================================
+ * Description: Contract for session persistence operations.
+ * ==============================================================================================
+ */
+package com.rho.studio.ui.core.domain.repository
+
+import com.rho.studio.ui.core.domain.model.User
+
+/**
+ * Interface defining the persistence operations for user sessions.
+ */
+interface SessionRepository {
+ suspend fun saveUser(user: User)
+ suspend fun getUser(): User?
+ suspend fun clearSession()
+}
diff --git a/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/usecase/LoginUseCase.kt b/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/usecase/LoginUseCase.kt
index bb2af5e..dbc35af 100644
--- a/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/usecase/LoginUseCase.kt
+++ b/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/usecase/LoginUseCase.kt
@@ -10,7 +10,7 @@
* File: LoginUseCase.kt
* Author: Alexis Tercero
* Email: alexis.tercero@rho.studio
- * Date: 2026-08-06
+ * Date: 2026-08-18
* ==============================================================================================
* Description: Orchestrates the authentication process by validating user credentials,
* interacting with the AuthRepository to verify identity, and updating the
@@ -22,23 +22,20 @@ package com.rho.studio.ui.core.domain.usecase
import com.rho.studio.ui.core.domain.model.Credentials
import com.rho.studio.ui.core.domain.model.User
import com.rho.studio.ui.core.domain.repository.AuthRepository
+import javax.inject.Inject
/*** Encapsulates the login business transaction.*/
-class LoginUseCase(
+class LoginUseCase @Inject constructor(
private val authRepository: AuthRepository,
private val sessionManager: SessionManagerInterface
) : BaseUseCase() {
override suspend fun execute(parameters: Credentials): User {
- // 1. Validate input using the Credentials domain model.
- if (!parameters.isValid) {
- throw IllegalArgumentException("Invalid credentials")
- }
-
- // 2. Authentication performed by Repository
+ // 1. Authentication performed by Repository
+ // Note: parameters (Credentials) are already validated via Value Objects
val user = authRepository.login(parameters)
- // 3. Update global session state
+ // 2. Update global session state
sessionManager.updateSession(user)
return user
diff --git a/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/usecase/LogoutUseCase.kt b/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/usecase/LogoutUseCase.kt
index 0005cf1..518705e 100644
--- a/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/usecase/LogoutUseCase.kt
+++ b/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/usecase/LogoutUseCase.kt
@@ -10,7 +10,7 @@
* File: LogoutUseCase.kt
* Author: Alexis Tercero
* Email: alexis.tercero@rho.studio
- * Date: 2026-08-06
+ * Date: 2026-08-18
* ==============================================================================================
* Description: Handles the termination of the user session by clearing authentication tokens,
* resetting global application state, and ensuring secure cleanup of
@@ -19,8 +19,10 @@
*/
package com.rho.studio.ui.core.domain.usecase
+import javax.inject.Inject
+
/*** Encapsulates the logout business transaction.*/
-class LogoutUseCase(
+class LogoutUseCase @Inject constructor(
private val sessionManager: SessionManagerInterface
) : BaseUseCase() {
diff --git a/core/domain/src/test/kotlin/com/rho/studio/ui/core/domain/usecase/LoginUseCaseTest.kt b/core/domain/src/test/kotlin/com/rho/studio/ui/core/domain/usecase/LoginUseCaseTest.kt
index 5e64b57..5291431 100644
--- a/core/domain/src/test/kotlin/com/rho/studio/ui/core/domain/usecase/LoginUseCaseTest.kt
+++ b/core/domain/src/test/kotlin/com/rho/studio/ui/core/domain/usecase/LoginUseCaseTest.kt
@@ -10,17 +10,16 @@
* File: LoginUseCaseTest.kt
* Author: Alexis Tercero
* Email: alexis.tercero@rho.studio
- * Date: 2026-08-06
+ * Date: 2026-08-12
* ==============================================================================================
- * Description: Check expected behavior of [LoginUseCase]
+ * Description: Check expected behavior of [LoginUseCase] with simplified credentials.
* ==============================================================================================
*/
package com.rho.studio.ui.core.domain.usecase
-import com.rho.studio.ui.core.domain.model.Credentials
-import com.rho.studio.ui.core.domain.model.Result
-import com.rho.studio.ui.core.domain.model.User
+import com.rho.studio.ui.core.domain.model.*
import com.rho.studio.ui.core.domain.repository.AuthRepository
+import io.mockk.*
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
@@ -32,54 +31,47 @@ class LoginUseCaseTest {
private lateinit var loginUseCase: LoginUseCase
private lateinit var authRepository: AuthRepository
private lateinit var sessionManager: SessionManagerInterface
- private var lastUpdatedUser: User? = null
- private var isSessionCleared = false
@Before
fun setUp() {
- sessionManager = object : SessionManagerInterface {
- override fun updateSession(user: User) {
- lastUpdatedUser = user
- }
- override fun clearSession() {
- isSessionCleared = true
- }
- override fun extractNameFromEmail(email: String): String {
- return "Test"
- }
- }
- authRepository = object : AuthRepository {
- override suspend fun login(credentials: Credentials): User {
- if (credentials.email == "error@rho.studio") throw RuntimeException("Network error")
- return User("user_123", credentials.email, "Test")
- }
- }
+ authRepository = mockk()
+ sessionManager = mockk(relaxed = true)
loginUseCase = LoginUseCase(authRepository, sessionManager)
}
@Test
- fun `login success with valid credentials`() = runBlocking {
+ fun `login success calls repository and updates session`() = runBlocking {
val credentials = Credentials("test@rho.studio", "password123")
+ val user = User("user_123", "test@rho.studio", "Test User")
+
+ coEvery { authRepository.login(credentials) } returns user
+
val result = loginUseCase(credentials)
+
assertTrue(result is Result.Success)
- val user = (result as Result.Success).data
- assertEquals("test@rho.studio", user.email)
- assertEquals("Test", user.name)
- assertEquals(user, lastUpdatedUser)
+ assertEquals(user, (result as Result.Success).data)
+ coVerify { sessionManager.updateSession(user) }
}
@Test
- fun `login failure with invalid credentials`() = runBlocking {
- val credentials = Credentials("invalid-email", "short")
+ fun `login failure returns error result`() = runBlocking {
+ val credentials = Credentials("test@rho.studio", "password123")
+ val exceptionMessage = "Auth failed"
+ val exception = RuntimeException(exceptionMessage)
+
+ coEvery { authRepository.login(credentials) } throws exception
+
val result = loginUseCase(credentials)
+
assertTrue(result is Result.Error)
- assertTrue((result as Result.Error).exception is IllegalArgumentException)
+ assertEquals(exceptionMessage, (result as Result.Error).exception.message)
+ coVerify(exactly = 0) { sessionManager.updateSession(any()) }
}
@Test
- fun `logout success`() = runBlocking {
+ fun `logout success calls session clear`() = runBlocking {
val logoutUseCase = LogoutUseCase(sessionManager)
logoutUseCase(Unit)
- assertTrue(isSessionCleared)
+ coVerify { sessionManager.clearSession() }
}
}
diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts
index 261b1c8..59f2271 100644
--- a/core/ui/build.gradle.kts
+++ b/core/ui/build.gradle.kts
@@ -1,6 +1,7 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.compose)
+ alias(libs.plugins.ksp)
}
android {
@@ -26,6 +27,10 @@ dependencies {
implementation(project(path = ":core:domain"))
implementation(project(path = ":core:data"))
+ // Dagger
+ implementation(libs.dagger)
+ ksp(libs.dagger.compiler)
+
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
diff --git a/core/ui/src/main/java/com/rho/studio/ui/core/ui/common/HeaderViewModel.kt b/core/ui/src/main/java/com/rho/studio/ui/core/ui/common/HeaderViewModel.kt
index 2aa66e7..a8020c9 100644
--- a/core/ui/src/main/java/com/rho/studio/ui/core/ui/common/HeaderViewModel.kt
+++ b/core/ui/src/main/java/com/rho/studio/ui/core/ui/common/HeaderViewModel.kt
@@ -1,5 +1,6 @@
/**
- * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * Rho Studio®
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗®
* ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
* ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
* ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
@@ -10,7 +11,7 @@
* File: HeaderViewModel.kt
* Author: Alexis Tercero
* Email: alexis.tercero@rho.studio
- * Date: 2026-08-06
+ * Date: 2026-08-18
* ==========================================================================
* Description:
* ViewModel for the reusable PageHeaderFragment.
@@ -24,13 +25,27 @@ package com.rho.studio.ui.core.ui.common
import com.rho.studio.ui.core.ui.base.BaseViewModel
import com.rho.studio.ui.core.data.manager.SessionManager
import com.rho.studio.ui.core.domain.model.User
+import com.rho.studio.ui.core.domain.model.SessionState
+import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+import javax.inject.Inject
-class HeaderViewModel : BaseViewModel() {
- private val sessionManager = SessionManager.getInstance()
- val currentUser: StateFlow = sessionManager.currentUser
+class HeaderViewModel @Inject constructor(
+ private val sessionManager: SessionManager
+) : BaseViewModel() {
+ private val _currentUser = MutableStateFlow(null)
+ val currentUser: StateFlow = _currentUser.asStateFlow()
+
+ init {
+ viewModelScope.launch {
+ sessionManager.sessionState.collect { state ->
+ _currentUser.value = (state as? SessionState.Authenticated)?.user
+ }
+ }
+ }
private val _title = MutableStateFlow("")
val title: StateFlow = _title.asStateFlow()
fun setTitle(newTitle: String) { _title.value = newTitle }
diff --git a/core/ui/src/main/java/com/rho/studio/ui/core/ui/di/DaggerViewModelFactory.kt b/core/ui/src/main/java/com/rho/studio/ui/core/ui/di/DaggerViewModelFactory.kt
new file mode 100644
index 0000000..cb4df94
--- /dev/null
+++ b/core/ui/src/main/java/com/rho/studio/ui/core/ui/di/DaggerViewModelFactory.kt
@@ -0,0 +1,47 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ==============================================================================================
+ * File: DaggerViewModelFactory.kt
+ * Author: Alexis Tercero
+ * Email: alexis.tercero@rho.studio
+ * Date: 2026-08-12
+ * ==============================================================================================
+ * Description: A custom ViewModelProvider.Factory that facilitates Dagger 2 multibinding.
+ * It acts as a bridge between Dagger's dependency injection container and
+ * Architecture Components' ViewModelStore, enabling constructor injection
+ * into ViewModels.
+ * ==============================================================================================
+ */
+package com.rho.studio.ui.core.ui.di
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import javax.inject.Inject
+import javax.inject.Provider
+
+/**
+ * Standard ViewModelFactory for Dagger multibinding.
+ */
+class DaggerViewModelFactory @Inject constructor(
+ private val creators: Map, @JvmSuppressWildcards Provider>
+) : ViewModelProvider.Factory {
+
+ override fun create(modelClass: Class): T {
+ val creator = creators[modelClass] ?: creators.entries.firstOrNull {
+ modelClass.isAssignableFrom(it.key)
+ }?.value ?: throw IllegalArgumentException("Unknown model class $modelClass")
+
+ try {
+ @Suppress("UNCHECKED_CAST")
+ return creator.get() as T
+ } catch (e: Exception) {
+ throw RuntimeException(e)
+ }
+ }
+}
diff --git a/core/ui/src/main/java/com/rho/studio/ui/core/ui/di/Scopes.kt b/core/ui/src/main/java/com/rho/studio/ui/core/ui/di/Scopes.kt
new file mode 100644
index 0000000..8f9efe7
--- /dev/null
+++ b/core/ui/src/main/java/com/rho/studio/ui/core/ui/di/Scopes.kt
@@ -0,0 +1,31 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ==============================================================================================
+ * File: Scopes.kt
+ * Author: Alexis Tercero
+ * Email: alexis.tercero@rho.studio
+ * Date: 2026-08-12
+ * ==============================================================================================
+ * Description: Defines custom Dagger scopes and qualifiers for dependency injection.
+ * Provides lifecycle management for components tied to specific app states,
+ * ensuring proper resource allocation and state persistence (e.g., User session)
+ * across the application architecture.
+ * ==============================================================================================
+ */
+package com.rho.studio.ui.core.ui.di
+
+import javax.inject.Scope
+import javax.inject.Qualifier
+
+/**
+ * Scope for dependencies that should live as long as the user is authenticated.
+ */
+@Scope
+@Retention(AnnotationRetention.RUNTIME)
+annotation class UserScope
diff --git a/core/ui/src/main/java/com/rho/studio/ui/core/ui/di/UIModule.kt b/core/ui/src/main/java/com/rho/studio/ui/core/ui/di/UIModule.kt
new file mode 100644
index 0000000..c70f610
--- /dev/null
+++ b/core/ui/src/main/java/com/rho/studio/ui/core/ui/di/UIModule.kt
@@ -0,0 +1,34 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ==============================================================================================
+ * File: UIModule.kt
+ * Author: Alexis Tercero
+ * Email: alexis.tercero@rho.studio
+ * Date: 2026-08-12
+ * ==============================================================================================
+ * Description: Dagger module for providing and binding UI-related dependencies.
+ * Manages ViewModel multi-bindings for the UI layer components.
+ * ==============================================================================================
+ */
+package com.rho.studio.ui.core.ui.di
+
+import androidx.lifecycle.ViewModel
+import com.rho.studio.ui.core.ui.common.HeaderViewModel
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoMap
+
+@Module
+abstract class UIModule {
+
+ @Binds
+ @IntoMap
+ @ViewModelKey(HeaderViewModel::class)
+ abstract fun bindHeaderViewModel(viewModel: HeaderViewModel): ViewModel
+}
diff --git a/core/ui/src/main/java/com/rho/studio/ui/core/ui/di/ViewModelKey.kt b/core/ui/src/main/java/com/rho/studio/ui/core/ui/di/ViewModelKey.kt
new file mode 100644
index 0000000..cd9079c
--- /dev/null
+++ b/core/ui/src/main/java/com/rho/studio/ui/core/ui/di/ViewModelKey.kt
@@ -0,0 +1,31 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ==============================================================================================
+ * File: ViewModelKey.kt
+ * Author: Alexis Tercero
+ * Email: alexis.tercero@rho.studio
+ * Date: 2026-08-12
+ * ==============================================================================================
+ * Description: Custom annotation used for Dagger ViewModel multibinding.
+ * Maps ViewModel classes to their respective providers within a Dagger Map.
+ * ==============================================================================================
+ */
+package com.rho.studio.ui.core.ui.di
+
+import androidx.lifecycle.ViewModel
+import dagger.MapKey
+import kotlin.reflect.KClass
+
+/**
+ * MapKey for Dagger ViewModel multibinding.
+ */
+@Target(AnnotationTarget.FUNCTION)
+@Retention(AnnotationRetention.RUNTIME)
+@MapKey
+annotation class ViewModelKey(val value: KClass)
diff --git a/features/auth/build.gradle.kts b/features/auth/build.gradle.kts
index 1137cb2..7dc9688 100644
--- a/features/auth/build.gradle.kts
+++ b/features/auth/build.gradle.kts
@@ -1,6 +1,7 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.compose)
+ alias(libs.plugins.ksp)
}
android {
@@ -26,6 +27,10 @@ dependencies {
implementation(project(path = ":core:domain"))
implementation(project(path = ":core:data"))
implementation(project(path = ":core:ui"))
+
+ // Dagger
+ implementation(libs.dagger)
+ ksp(libs.dagger.compiler)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
diff --git a/features/auth/src/main/java/com/rho/studio/ui/features/auth/LoginViewModel.kt b/features/auth/src/main/java/com/rho/studio/ui/features/auth/LoginViewModel.kt
index 4995dc4..82d77c6 100644
--- a/features/auth/src/main/java/com/rho/studio/ui/features/auth/LoginViewModel.kt
+++ b/features/auth/src/main/java/com/rho/studio/ui/features/auth/LoginViewModel.kt
@@ -10,46 +10,11 @@
* File: LoginViewModel.kt
* Author: Alexis Tercero
* Email: alexis.tercero@rho.studio
- * Date: 2026-08-06
+ * Date: 2026-08-18
* ============================================================================
* Description:
* The LoginViewModel manages the state and business logic for the
- * Authentication screen, acting as the UI Layer in a pure
- * Jetpack Compose environment.
- * It leverages the Rho Studio BaseViewModel architecture to handle
- * user input validation, asynchronous login requests via LoginUseCase,
- * and reactive UI states using both Compose State and StateFlow.
- *
- * •Extends: com.rho.studio.ui.core.ui.base.BaseViewModel
- * •Dependencies:s
- * •LoginUseCase: Orchestrates the login flow through the Repository.
- * •Credentials: A data model encapsulating email and password logic.
- *
- * Core Logic Flows
- * State-Driven Input
- * •email / password: Uses Compose `mutableStateOf` to provide
- * immediate, observable reactivity for the UI layer.
- * Real-time & Debounced Validation
- * •onEmailChanged() / onPasswordChanged():
- * Triggered on every keystroke, updating the state immediately.
- * •300ms Debounce: Logic moved from Fragments to the ViewModel,
- * ensuring validation is only performed after the user pauses typing.
- * Authentication Process
- * •Trigger: onLoginClick() performs final validation and guards
- * against concurrent attempts using the base loading state.
- * •Execution: performLogin() utilizes launchWithLoading() to
- * automatically manage the UI loading state and error trapping.
- * •UseCase: Executes loginUseCase(credentials) within a managed coroutine.
- * •Result Handling:
- * •Success: Sets success toast; navigation is handled via SessionManager state.
- * •Failure: Customizes error messages via the handleError() hook.
- * Layering & Architecture
- * •Job Management:
- * Relies on BaseViewModel's automated job tracking and cleanup
- * to prevent memory leaks without manual cancellation logic.
- * •State Reset:
- * resetForm() provides a clean, secure slate for the UI by
- * clearing Compose states and the underlying model.
+ * Authentication screen, utilizing reactive validation and Firebase.
* ============================================================================
*/
package com.rho.studio.ui.features.auth
@@ -62,92 +27,89 @@ import com.rho.studio.ui.core.ui.base.BaseViewModel
import com.rho.studio.ui.core.domain.model.Credentials
import com.rho.studio.ui.core.domain.model.Result
import com.rho.studio.ui.core.domain.usecase.LoginUseCase
-import com.rho.studio.ui.core.data.manager.SessionManager
-import com.rho.studio.ui.core.data.repository.AuthRepositoryImpl
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
+import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
-class LoginViewModel : BaseViewModel() {
- // ==================== DEPENDENCIES ====================
- private val loginUseCase = LoginUseCase(AuthRepositoryImpl(), SessionManager.getInstance())
+class LoginViewModel @Inject constructor(
+ private val loginUseCase: LoginUseCase
+) : BaseViewModel() {
+
private var loginJob: Job? = null
- private var emailDebounceJob: Job? = null
- private var passwordDebounceJob: Job? = null
+ private var validationJob: Job? = null
+
// ==================== FORM STATE ====================
var email by mutableStateOf("")
private set
var password by mutableStateOf("")
private set
- val credentials = Credentials()
+
// ==================== UI STATE ====================
private val _emailError = MutableStateFlow(null)
val emailError: StateFlow = _emailError.asStateFlow()
+
private val _passwordError = MutableStateFlow(null)
val passwordError: StateFlow = _passwordError.asStateFlow()
+
private val _isFormValid = MutableStateFlow(false)
val isFormValid: StateFlow = _isFormValid.asStateFlow()
- // ==================== FORM VALIDATION ====================
+ companion object {
+ private val EMAIL_REGEX = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$".toRegex()
+ }
+
+ // ==================== ACTIONS ====================
fun onEmailChanged(email: String) {
this.email = email
- credentials.email = email
-
- emailDebounceJob?.cancel()
- emailDebounceJob = viewModelScope.launch {
- delay(300.milliseconds)
- validateEmail()
- validateForm()
- }
+ triggerValidation()
}
fun onPasswordChanged(password: String) {
this.password = password
- credentials.password = password
-
- passwordDebounceJob?.cancel()
- passwordDebounceJob = viewModelScope.launch {
+ triggerValidation()
+ }
+
+ private fun triggerValidation() {
+ validationJob?.cancel()
+ validationJob = viewModelScope.launch {
delay(300.milliseconds)
- validatePassword()
- validateForm()
+ validate()
}
}
- private fun validateEmail() {
+ private fun validate(): Boolean {
+ val emailValid = email.isNotBlank() && email.matches(EMAIL_REGEX)
+
_emailError.value = when {
- credentials.email.isBlank() -> "Email is required"
- !credentials.isEmailValid -> "Please enter a valid email address"
+ email.isBlank() -> "Email is required"
+ !emailValid -> "Please enter a valid email address"
else -> null
}
- }
- private fun validatePassword() {
_passwordError.value = when {
- credentials.password.isBlank() -> "Password is required"
- !credentials.isPasswordValid -> "Password must be at least 6 characters"
+ password.isBlank() -> "Password is required"
else -> null
}
- }
- private fun validateForm() {
- _isFormValid.value = credentials.isValid
+ val isValid = emailValid //&& passwordValid
+ _isFormValid.value = isValid
+ return isValid
}
fun onLoginClick() {
if (isLoading.value) return
- if (!credentials.isValid) {
- validateEmail()
- validatePassword()
- return
+
+ if (validate()) {
+ performLogin(Credentials(email, password))
}
- performLogin()
}
- private fun performLogin() {
+ private fun performLogin(credentials: Credentials) {
loginJob = launchWithLoading(
block = {
when (val result = loginUseCase(credentials)) {
@@ -167,7 +129,6 @@ class LoginViewModel : BaseViewModel() {
fun resetForm() {
email = ""
password = ""
- credentials.clear()
_emailError.value = null
_passwordError.value = null
_isFormValid.value = false
diff --git a/features/auth/src/main/java/com/rho/studio/ui/features/auth/di/AuthModule.kt b/features/auth/src/main/java/com/rho/studio/ui/features/auth/di/AuthModule.kt
new file mode 100644
index 0000000..d3a4c48
--- /dev/null
+++ b/features/auth/src/main/java/com/rho/studio/ui/features/auth/di/AuthModule.kt
@@ -0,0 +1,35 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ==============================================================================================
+ * File: AuthModule.kt
+ * Author: Alexis Tercero
+ * Email: alexis.tercero@rho.studio
+ * Date: 2026-08-12
+ * ==============================================================================================
+ * Description: Dagger module for the authentication feature.
+ * Provides and binds dependencies related to auth UI components and ViewModels.
+ * ==============================================================================================
+ */
+package com.rho.studio.ui.features.auth.di
+
+import androidx.lifecycle.ViewModel
+import com.rho.studio.ui.core.ui.di.ViewModelKey
+import com.rho.studio.ui.features.auth.LoginViewModel
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoMap
+
+@Module
+abstract class AuthModule {
+
+ @Binds
+ @IntoMap
+ @ViewModelKey(LoginViewModel::class)
+ abstract fun bindLoginViewModel(viewModel: LoginViewModel): ViewModel
+}
diff --git a/features/home/build.gradle.kts b/features/home/build.gradle.kts
index 0fe76db..fe84218 100644
--- a/features/home/build.gradle.kts
+++ b/features/home/build.gradle.kts
@@ -1,6 +1,7 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.compose)
+ alias(libs.plugins.ksp)
}
android {
@@ -26,6 +27,10 @@ dependencies {
implementation(project(path = ":core:domain"))
implementation(project(path = ":core:data"))
implementation(project(path = ":core:ui"))
+
+ // Dagger
+ implementation(libs.dagger)
+ ksp(libs.dagger.compiler)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
diff --git a/features/home/src/main/java/com/rho/studio/ui/features/home/HomeViewModel.kt b/features/home/src/main/java/com/rho/studio/ui/features/home/HomeViewModel.kt
index 7d3e6e0..f0e16b6 100644
--- a/features/home/src/main/java/com/rho/studio/ui/features/home/HomeViewModel.kt
+++ b/features/home/src/main/java/com/rho/studio/ui/features/home/HomeViewModel.kt
@@ -10,7 +10,7 @@
* File: HomeViewModel.kt
* Author: Alexis Tercero
* Email: alexis.tercero@rho.studio
- * Date: 2026-08-06
+ * Date: 2026-08-18
* ==============================================================================================
* Description: ViewModel for the Home feature, managing feature state, session data,
* and providing access to available service modules.
@@ -27,13 +27,14 @@ import com.rho.studio.ui.core.domain.usecase.LogoutUseCase
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
+import javax.inject.Inject
-class HomeViewModel : BaseViewModel() {
+class HomeViewModel @Inject constructor(
+ private val sessionManager: SessionManager,
+ private val logoutUseCase: LogoutUseCase
+) : BaseViewModel() {
- private val sessionManager = SessionManager.getInstance()
- private val logoutUseCase = LogoutUseCase(sessionManager)
-
- private val _currentUser = MutableStateFlow(sessionManager.getCurrentUserSync())
+ private val _currentUser = MutableStateFlow(sessionManager.getCurrentUser())
val currentUser: StateFlow = _currentUser.asStateFlow()
// Parametrized services for the Home experience
diff --git a/features/home/src/main/java/com/rho/studio/ui/features/home/di/HomeModule.kt b/features/home/src/main/java/com/rho/studio/ui/features/home/di/HomeModule.kt
new file mode 100644
index 0000000..8df6e3d
--- /dev/null
+++ b/features/home/src/main/java/com/rho/studio/ui/features/home/di/HomeModule.kt
@@ -0,0 +1,35 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ==============================================================================================
+ * File: HomeModule.kt
+ * Author: Alexis Tercero
+ * Email: alexis.tercero@rho.studio
+ * Date: 2026-08-14
+ * ==============================================================================================
+ * Description: Dagger module responsible for defining dependency injections for the Home feature.
+ * Provides bindings for the HomeViewModel and other home-scoped dependencies.
+ * ==============================================================================================
+ */
+package com.rho.studio.ui.features.home.di
+
+import androidx.lifecycle.ViewModel
+import com.rho.studio.ui.core.ui.di.ViewModelKey
+import com.rho.studio.ui.features.home.HomeViewModel
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoMap
+
+@Module
+abstract class HomeModule {
+
+ @Binds
+ @IntoMap
+ @ViewModelKey(HomeViewModel::class)
+ abstract fun bindHomeViewModel(viewModel: HomeViewModel): ViewModel
+}
diff --git a/gradle.properties b/gradle.properties
index ae9a532..7324efd 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -31,3 +31,4 @@ android.r8.strictFullModeForKeepRules=false
android.r8.optimizedResourceShrinking=true
android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false
android.sync.suppressAgpWarnings=LIBRARY_CONSTRAINTS_SHOULD_BE_DISABLED,UNSUPPORTED_PROJECT_OPTION_USE
+android.disallowKotlinSourceSets=false
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 124494d..f6be5fa 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -3,8 +3,8 @@ agp = "9.3.1"
compiler = "3.1.4"
fragmentKtx = "1.8.9"
gson = "2.14.0"
-kotlin = "2.4.10"
-kotlinParcelize = "2.4.10"
+kotlin = "2.0.21"
+kotlinParcelize = "2.0.21"
coreKtx = "1.19.0"
junit = "4.13.2"
junitVersion = "1.3.0"
@@ -15,6 +15,11 @@ activityCompose = "1.13.0"
composeBom = "2026.06.01"
material = "1.14.0"
navigation = "2.9.8"
+dagger = "2.60.1"
+firebaseBom = "34.17.0"
+datastore = "1.2.1"
+coroutinesPlayServices = "1.11.0"
+mockk = "1.14.11"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -40,6 +45,14 @@ androidx-compose-material-icons-extended = { group = "androidx.compose.material"
androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigation" }
androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigation" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
+dagger = { group = "com.google.dagger", name = "dagger", version.ref = "dagger" }
+dagger-compiler = { group = "com.google.dagger", name = "dagger-compiler", version.ref = "dagger" }
+firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" }
+firebase-auth = { group = "com.google.firebase", name = "firebase-auth" }
+firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics" }
+datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
+kotlinx-coroutines-play-services = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-play-services", version.ref = "coroutinesPlayServices" }
+mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
@@ -48,3 +61,5 @@ kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
legacy-kapt = { id = "com.android.legacy-kapt", version.ref = "agp" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlinParcelize" }
+ksp = { id = "com.google.devtools.ksp", version = "2.0.21-1.0.28" }
+googleServices = { id = "com.google.gms.google-services", version = "4.5.0" }