diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 25ab4b1..f57bac3 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -1,11 +1,11 @@ -name: Android CI/CD Rho.Studio® +name: Android Debug CI/CD Rho.Studio® on: workflow_dispatch: push: branches: [ " " ] pull_request: - branches: [ "dev" , "pre-release" , "main" ] + branches: [ "dev" , "pre-release" ] jobs: build: @@ -25,11 +25,22 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew + + - name: Run Unit Tests + run: ./gradlew test + - name: Build with Gradle - run: ./gradlew build + run: ./gradlew assembleDebug - name: Upload APK uses: actions/upload-artifact@v7.0.1 with: name: app-debug path: app/build/outputs/apk/debug/app-debug.apk + + - name: Upload Test Reports + if: failure() + uses: actions/upload-artifact@v7.0.1 + with: + name: test-reports + path: "**/build/reports/tests/" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..c586309 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,52 @@ +name: Android Release Rho.Studio® + +on: + workflow_dispatch: + push: + branches: [ "Pre-release-v102" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + name: Build and Release Rho.Studio® + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v6.1.0 + + - name: Set up JDK 17 + uses: actions/setup-java@v5.5.0 + with: + java-version: '17' + distribution: 'temurin' + cache: gradle + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Run Unit Tests + run: ./gradlew test + + - name: Build with Gradle + run: ./gradlew assembleDebug + + - name: List APK directory + run: ls -la app/build/outputs/apk/debug/ + + - name: Rename APK + run: mv app/build/outputs/apk/debug/app-debug.apk app/build/outputs/apk/debug/RhoStudioUI.apk + + - name: Upload APK + uses: actions/upload-artifact@v7.0.1 + with: + name: Rho-Studio-UI + path: app/build/outputs/apk/debug/RhoStudioUI.apk + + - name: Upload Test Reports + if: failure() + uses: actions/upload-artifact@v7.0.1 + with: + name: test-reports + path: "**/build/reports/tests/" diff --git a/CONTRIBUTION.md b/CONTRIBUTION.md new file mode 100644 index 0000000..3ea1c6a --- /dev/null +++ b/CONTRIBUTION.md @@ -0,0 +1,70 @@ +# 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 39adf0e..8558d41 100644 --- a/README.md +++ b/README.md @@ -1,104 +1,352 @@ -# Technical Report: Rho Studio UI Architecture -## Modern Android Development with Jetpack Compose & MVVM -This report outlines the architecture design of the Rho Studio UI application. +# Technical Report: Rho Studio UI +An Android Jetpack Compose app. -Image +[![Android CI/CD Rho.Studio®](https://github.com/Rho-Studio/UI-Utils-Rho-Studio/actions/workflows/android.yml/badge.svg)](https://github.com/Rho-Studio/UI-Utils-Rho-Studio/actions/workflows/android.yml) +[![Android Release Rho.Studio®](https://github.com/Rho-Studio/UI-Utils-Rho-Studio/actions/workflows/release.yml/badge.svg)](https://github.com/Rho-Studio/UI-Utils-Rho-Studio/actions/workflows/release.yml) + +> _Document Version: 2.0 Last Updated: August 10, 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. + +App Screenshot --- ## 1. Executive Summary -The application is a pure **Jetpack Compose** implementation following a **Single-Activity Architecture**. It leverages 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). +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**. + +### 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. --- -## 2. Integrated Architectural Perspective -The project utilizes a **Feature-Layered Architecture**. Each feature is encapsulated within its own package, maintaining a clean internal separation between UI (Compose) and Logic (ViewModels), while sharing a common Core/Data foundation. +## 2. Architectural Framework +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 UI & Feature Layers (View) -The UI is composed of stateless screens and modular components that observe state from their respective ViewModels. +### 2.1 Layered Structure +The system is divided into three primary logical layers, enforcing a strict unidirectional dependency flow: **UI → Domain ← Data**. -- **`MainActivity.kt`**: The application's core orchestrator. Manages the high-level `NavHost`, coordinates the global `LoadingOverlay`, and synchronizes navigation via `SessionManager`. -- **Authentication Feature (`features/auth/`)**: - - `LoginScreen.kt`: The main entry point for user authentication. - - `LoginEmailField.kt` / `LoginPasswordField.kt`: Specialized inputs with built-in validation and security logic. -- **Home & Dashboard Feature (`features/home/`)**: - - `HomeScreen.kt`: The primary post-auth landing page. - - `ServiceList.kt` / `ServiceItem.kt`: Adaptive components for dynamic content delivery. -- **Common UI Feature (`features/common/`)**: - - `PageHeader.kt` / `PageFooter.kt`: Shared layouts that provide global context and actions (e.g., Logout). +```mermaid +graph TD + subgraph "UI Layer (Presentation)" + UI[Jetpack Compose Screens] + VM[ViewModels] + Nav[Navigation / NavHost] + end -### 2.2 Business Logic & State Layer (ViewModel) -ViewModels act as the bridge between features and the data layer, handling user intent and reactive state. + subgraph "Domain Layer (Business Logic)" + UC[Use Cases / Interactors] + Entities[Domain Entities] + Int[Repository Interfaces] + end -- **`BaseViewModel.kt`**: The architectural anchor providing unified loading states, toast messaging, and standardized error handling. -- **`LoginViewModel.kt`**: Manages complex form state and **debounced validation** logic. -- **`HomeViewModel.kt`**: Orchestrates dashboard content lifecycle and session termination. -- **`HeaderViewModel.kt`**: Bridges the `SessionManager` state to common UI components like the `PageHeader`. + subgraph "Data Layer (Infrastructure)" + Repo[Repository Implementations] + SM[Session Manager] + Local[Local / Network Data Sources] + end -### 2.3 Core Data & Infrastructure Layer -Provides the essential services and "Single Source of Truth" for the entire application. + UI --> VM + VM --> UC + UC --> Entities + UC --> Int + Repo -.-> Int + Repo --> SM + Repo --> Local +``` -- **`SessionManager.kt`**: A singleton coordinator for the application's global authentication state and user profile. -- **`SessionRepository.kt`**: Manages persistent storage and retrieval of session tokens and user data. -- **`Credentials.kt` / `User.kt` / `ServiceModule.kt`**: Strongly typed data models that enforce business rules and schema consistency. +### 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. +```mermaid +flowchart TD + APP[":app
MainActivity, NavHost"] + + AUTH[":features:auth
LoginScreen, LoginViewModel"] + HOME[":features:home
HomeScreen, HomeViewModel"] + + UI_CORE[":core:ui
Theme, Common Composables"] + DOMAIN[":core:domain
Use Cases, Models, Contracts"] + DATA[":core:data
Repositories, SessionManager"] + + APP --> AUTH + APP --> HOME + + AUTH --> UI_CORE + AUTH --> DOMAIN + HOME --> UI_CORE + HOME --> DOMAIN + + UI_CORE --> DOMAIN + + DOMAIN -.->|"implemented by"| DATA + + 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 +``` +> **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. --- -## 3. Core Technical Implementations - -### 3.1 State-Driven Reactive Navigation -Navigation is decoupled from direct user input. `MainActivity.kt` observes the `isAuthenticated` state from `SessionManager.kt`. When this state changes, a `LaunchedEffect` executes the transition, ensuring the UI is always a reflection of the underlying session state. +## 3. Layer Detail & Responsibilities -### 3.2 Performance Optimized Validation -To ensure a smooth typing experience, `LoginViewModel.kt` utilizes **Coroutine Debouncing**. Input validation is deferred until the user pauses for 300ms, minimizing unnecessary UI updates and logic execution. +### 3.1 UI Layer (Presentation) +**Goal**: Transform application state into a visual interface and handle user interactions. +- **Jetpack Compose**: All UI is declarative, using stateless composables for maximum testability. +- **MVVM Pattern**: ViewModels manage UI state using `StateFlow`, exposing it to the UI in a lifecycle-aware manner. +- **UDF (Unidirectional Data Flow)**: User actions trigger events in the ViewModel, which updates the state, triggering a UI recomposition. +- **Side-Effect Orchestration**: `MainActivity` uses `LaunchedEffect` keyed to authentication state, transforming state changes into one-time navigation events. +- **Key Components**: + - `MainActivity.kt`: The entry point and navigation orchestrator. + - `LoginViewModel.kt` & `HomeViewModel.kt`: Feature-specific state holders. + - `BaseViewModel.kt`: Provides shared logic for loading states, error handling, and navigation side-effects. + - `HeaderViewModel.kt`: Bridges `SessionManager` state to common UI components +```mermaid +flowchart TB + subgraph Navigation["Navigation Orchestration"] + MA["MainActivity.kt
- NavHost
- Session-based routing"] + end + + subgraph Shared["Shared UI Components"] + PV["BaseViewModel.kt
- Loading states
- Error handling"] + 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"] + end + + subgraph Home["Home Feature"] + HS["HomeScreen.kt"] + HVM["HomeViewModel.kt
- Home state
- Session termination"] + end + + MA --> LS + MA --> HS + LS --> LVM + HS --> HVM + 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 +``` -### 3.3 Centralized Design System -Managed in `ui/theme/`, the app uses a custom Material 3 implementation. This ensures brand consistency (`RhoRed`, `RhoStrongGray`) is automatically applied to all features through a unified `Theme.kt` and `Color.kt` definition. +### 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. +- **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. +### 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. +```mermaid +flowchart TB + subgraph SSOT["Single Source of Truth"] + SM["SessionManager.kt
- AuthState Flow
- updateSession()
- clearSession()"] + end + + subgraph Repos["Repository Implementations"] + ARI["AuthRepositoryImpl
- Mock login()"] + SRI["SessionRepositoryImpl
- SharedPreferences"] + end + + subgraph Sources["Data Sources (Planned)"] + Remote["Remote API
- Firebase Auth"] + Local["Local Storage
- Room Database"] + end + + SM --> SRI + ARI --> Remote + SRI --> Local + + style SSOT fill:#e94560,stroke:#c62828,color:#ffffff + style Repos fill:#1a1a2e,stroke:#e94560,color:#ffffff + style Sources fill:#0f3460,stroke:#16213e,color:#ffffff +``` --- -## 4. File Registry & Responsibilities +## 4. Technical Implementation Standards -| File | Feature | Primary Engineering Responsibility | -| :--- | :--- | :--- | -| `MainActivity.kt` | App Root | Global orchestration, NavHost, and session-based routing. | -| `BaseViewModel.kt` | Core | Shared architectural logic for Loading/Error states. | -| `SessionManager.kt` | Core | Centralized authentication and session lifecycle management. | -| `LoginViewModel.kt` | Auth | Form state management and debounced validation. | -| `HomeScreen.kt` | Home | Root layout for the post-authentication dashboard. | -| `ServiceList.kt` | Home | Efficient grid implementation for platform modules. | -| `Credentials.kt` | Core | Logic-heavy model for credential validation rules. | -| `Theme.kt` | Design | Global Material 3 theme configuration and brand mapping. | +### 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. +```mermaid +sequenceDiagram + participant UI as MainActivity + participant SM as SessionManager + participant Nav as NavController + + 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 +``` ---- +### 4.2 Modularization Strategy +The project is split into granular Gradle modules to improve build times and enforce architectural boundaries: +- `:app`: The main coordinator and DI root. +- `:features:*`: Feature-specific UI and ViewModels (e.g., `:features:auth`, `:features:home`). +- `:core:ui`: Shared design system components and theming. +- `:core:domain`: The platform-agnostic business layer. +- `:core:data`: Implementation details for data and external services. + +### 4.3 Design System +Located in `:core:ui`, the design system defines the application's visual language: +- **Typography**: Custom typeface integration. +- **Color Palette**: Strict adherence to the Rho Studio brand (`RhoRed`, `RhoStrongGray`). +- **Components**: A library of reusable, styleable components (Buttons, Inputs, Cards). -## 5. Path to Enterprise-Grade Architecture +--- -To transition this foundation into a highly scalable, enterprise-grade application, the following architectural advancements are planned to manage complex business flows and transactional integrity. +## 5. Roadmap & Evolution: Strategic Phases -### 5.1 Domain Layer & Use Case Implementation -As business logic complexity grows, direct ViewModel-to-Repository interaction is being transitioned to a dedicated **Domain Layer**. -- **Use Cases (Interactors)**: Classes like `LoginUseCase.kt` (`core/domain/usecase/LoginUseCase.kt`) encapsulate specific business rules, making the logic reusable across different ViewModels and testable in isolation. -- **Business Transaction Flow**: A single user action (e.g., "Login") may involve multiple steps: credential validation -> token acquisition -> user profile synchronization. These are managed as atomic transactions within the Domain Layer. -- **Best Practice**: [Android Guide to the Domain Layer](https://developer.android.com/topic/architecture/domain-layer) +The application is transitioning from a modular prototype to a production-hardened system. The evolution is structured into three strategic phases: -### 5.2 Advanced Data Flow & Synchronization -Enterprise apps require robust data handling beyond simple memory state. -- **Repository Pattern**: Refined `SessionRepository.kt` and future repositories will implement a **Single Source of Truth (SSOT)** strategy, coordinating between local storage (Room) and remote APIs (Retrofit). -- **Reactive Stream Transactions**: Utilizing **Kotlin Flow** for end-to-end reactive streams. Transactions are modeled as immutable states flowing from the Data Layer to the UI. -- **Best Practice**: [Data Layer with Repositories](https://developer.android.com/topic/architecture/data-layer) +### Phase I: Dependency Orchestration & Decoupling +- **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. +- **Interface Segregation**: Strict enforcement of domain-defined interfaces to further isolate the Data Layer from Business Logic. -### 5.3 Scalability & Reliability Standards -- **Dependency Injection (Hilt)**: Moving from manual singleton management to **Dagger Hilt** for better decoupling and automated lifecycle management. -- **Modularization**: Splitting the current feature packages into independent Gradle modules (`:feature:auth`, `:feature:home`, `:core:data`) to improve build times and enforce strict visibility boundaries. -- **Best Practice**: [Guide to App Modularization](https://developer.android.com/topic/modularization) +### Phase II: Transactional Integrity & persistence +- **Advanced Token Management**: + - 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] + B[2. Persistence
SessionRepository.saveToken] + C[3. Validation
ValidateTokenUseCase] + D[4. Refresh
RefreshTokenUseCase] + E[5. Recovery
SessionManager.initializeSession] + F[6. Invalidation
LogoutUseCase] + + A --> B --> C + C -->|"Valid"| G[Use Access Token] + C -->|"Expired"| D --> B + 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 +``` +### 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. +```mermaid +flowchart LR + subgraph Current["Current Flow"] + C1[UI] --> C2[ViewModel] --> C3[UseCase] --> C4[Repository] --> C5[SharedPreferences/Mock Auth] + end + + subgraph Planned["Planned Flow"] + P1[UI] --> P2[ViewModel] --> P3[UseCase] --> P4[Repository] + P4 --> P5[Local: Room Database] + P4 --> P6[Remote: Retrofit/Firebase] + end + + Current -.->|"Evolution"| Planned + + style Current fill:#1a1a2e,stroke:#e94560,color:#ffffff + style Planned fill:#0f3460,stroke:#16213e,color:#ffffff +``` --- -## 6. References & Standards +## 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/jetpack/compose/architecture#udf) principles for state management. -- **Clean Architecture**: Implementing principles from Robert C. Martin to maintain a high degree of testability and independence from external libraries. [Clean Architecture Reference](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) +- **Jetpack Compose Best Practices**: Following UDF ([Unidirectional Data Flow](https://developer.android.com/develop/ui/compose/architecture#udf)) principles for state management. +- **Multi-Module Topology**: Following [Guide to App Modularization](https://developer.android.com/topic/modularization). +- **Dependency Injection**: [Dagger Documentation](https://dagger.dev/). +- **Secure Token Management**: [Android Security Best Practices](https://developer.android.com/privacy-and-security/security-best-practices). + --- -**[Rho.Studio®](https://rho.studio/) - Engineering Department** - Contact [alexis.tercero@rho.studio](mailto:alexis.tercero@rho.studio) +**[Rho.Studio®](https://rho.studio/) - Engineering Department** - Contact [alexis.tercero@rho.studio](mailto:alexis.tercero@rho.studio) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cc9120e..7c321b8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -35,21 +35,6 @@ android { buildFeatures { compose = true } - - sourceSets { - getByName("main") { - // Java/Kotlin source directories - java.srcDirs( - "src/main/java" // This includes everything under java/ - // No need to list each feature separately - ) - - // Resource directories - THIS IS KEY FOR FEATURE RESOURCES - res.srcDirs( - "src/main/res" // Global resources - ) - } - } } // Add the new DSL here @@ -60,6 +45,11 @@ kotlin { } dependencies { + implementation(project(path = ":core:domain")) + implementation(project(path = ":core:data")) + implementation(project(path = ":core:ui")) + implementation(project(path = ":features:auth")) + implementation(project(path = ":features:home")) implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) @@ -71,7 +61,6 @@ dependencies { implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) implementation(libs.androidx.compose.material.icons.extended) - implementation(libs.androidx.compose.runtime.livedata) implementation(libs.androidx.navigation.compose) implementation(libs.gson) implementation(libs.material) @@ -82,4 +71,4 @@ dependencies { androidTestImplementation(libs.androidx.ui.test.junit4) debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/rho/studio/ui/MainActivity.kt b/app/src/main/java/com/rho/studio/ui/MainActivity.kt index 1bd3030..cda9f57 100644 --- a/app/src/main/java/com/rho/studio/ui/MainActivity.kt +++ b/app/src/main/java/com/rho/studio/ui/MainActivity.kt @@ -10,7 +10,7 @@ * File: MainActivity.kt * Author: Alexis Tercero * Email: alexis.tercero@rho.studio - * Date: 2026-07-29 + * Date: 2026-08-04 * ========================================================================== * Description: * The primary entry point for the RHO Studio application, migrated to @@ -19,6 +19,8 @@ * Main Entry Point: Migrated MainActivity to ComponentActivity. * Compose Navigation: Implemented a NavHost in MainActivity to handle routing * based on SessionManager state, replacing nav_graph.xml. + * State Management: Switched state observation from LiveData to StateFlow + * using collectAsState() for better compatibility with Compose. * ========================================================================== */ package com.rho.studio.ui @@ -32,20 +34,22 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import com.rho.studio.ui.core.manager.SessionManager +import com.rho.studio.ui.core.data.manager.SessionManager import com.rho.studio.ui.features.auth.LoginScreen import com.rho.studio.ui.features.auth.LoginViewModel import com.rho.studio.ui.features.home.HomeScreen import com.rho.studio.ui.features.home.HomeViewModel -import com.rho.studio.ui.ui.theme.UITheme +import com.rho.studio.ui.core.ui.theme.UITheme +import kotlinx.coroutines.launch class MainActivity : ComponentActivity() { @@ -55,9 +59,7 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - initializeManagers() - setContent { UITheme { MainContent() @@ -71,10 +73,12 @@ class MainActivity : ComponentActivity() { loginViewModel = ViewModelProvider(this)[LoginViewModel::class.java] homeViewModel = ViewModelProvider(this)[HomeViewModel::class.java] - sessionManager.error.observe(this) { error -> - error?.let { - Toast.makeText(this, it, Toast.LENGTH_LONG).show() - sessionManager.clearError() + lifecycleScope.launch { + sessionManager.error.collect { error -> + error?.let { + Toast.makeText(this@MainActivity, it, Toast.LENGTH_LONG).show() + sessionManager.clearError() + } } } } @@ -82,16 +86,15 @@ class MainActivity : ComponentActivity() { @Composable private fun MainContent() { val navController = rememberNavController() - val isSessionChecked by sessionManager.isSessionChecked.observeAsState(false) - val isAuthenticated by sessionManager.isAuthenticated.observeAsState(false) - val isLoading by sessionManager.isLoading.observeAsState(false) + val isSessionChecked by sessionManager.isSessionChecked.collectAsState() + val isAuthenticated by sessionManager.isAuthenticated.collectAsState() + val isLoading by sessionManager.isLoading.collectAsState() if (!isSessionChecked) { LoadingScreen() return } - // Handle navigation based on auth state LaunchedEffect(isAuthenticated) { if (isAuthenticated) { navController.navigate("home") { @@ -145,4 +148,4 @@ class MainActivity : ComponentActivity() { super.onDestroy() sessionManager.cleanup() } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/rho/studio/ui/core/manager/SessionManager.kt b/app/src/main/java/com/rho/studio/ui/core/manager/SessionManager.kt deleted file mode 100644 index 50cba10..0000000 --- a/app/src/main/java/com/rho/studio/ui/core/manager/SessionManager.kt +++ /dev/null @@ -1,237 +0,0 @@ -/** - * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗ - * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗ - * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║ - * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║ - * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝ - * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ - * - * ============================================================================================== - * File: SessionManager.kt - * Author: Alexis Tercero - * Email: alexis.tercero@rho.studio - * Date: 2026-07-14 - * ============================================================================================== - * Description: Singleton orchestrator for authentication state and session lifecycle. - * Delegates persistence to a SessionRepository - * and exposes reactive state via LiveData. - * ============================================================================================== - */ -package com.rho.studio.ui.core.manager - -import android.content.Context -import android.util.Log -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import com.rho.studio.ui.core.model.User -import com.rho.studio.ui.core.repository.SessionRepository -import com.rho.studio.ui.core.repository.SessionRepositoryImpl -import kotlinx.coroutines.* - -/** - * # SessionManager - * Core module - Singleton orchestrator for authentication state and session lifecycle. - * Delegates persistence to a SessionRepository and exposes reactive state via LiveData. - */ -class SessionManager private constructor() { - - companion object { - private const val TAG = "SessionManager" - - /** 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 - - /** - * 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)) - } - } - - // ==================== 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 mutable LiveData - * Only the SessionManager can modify the state*/ - private val _isAuthenticated = MutableLiveData(false) - private val _currentUser = MutableLiveData(null) - private val _isLoading = MutableLiveData(false) - private val _error = MutableLiveData(null) - private val _isSessionChecked = MutableLiveData(false) - - /** # PUBLIC LIVEDATA - * The rest of the app (UI/ViewModels) can only observe the state.*/ - val isAuthenticated: LiveData = _isAuthenticated - val currentUser: LiveData = _currentUser - val isLoading: LiveData = _isLoading - val error: LiveData = _error - val isSessionChecked: LiveData = _isSessionChecked - - // ==================== INITIALIZATION ==================== - - private fun initialize(repository: SessionRepository) { - if (isInitialized) return - - this.repository = repository - isInitialized = true - loadSavedSession() - } - - private fun checkInitialized() { - if (!isInitialized) { - throw IllegalStateException("SessionManager must be initialized with init(context) before use.") - } - } - - // ==================== PUBLIC METHODS ==================== - - /** - * Authenticate user with credentials (mock implementation). - */ - fun login(email: String, password: String): Job { - checkInitialized() - _isLoading.postValue(true) - _error.postValue(null) - - return sessionScope.launch { - try { - delay(1500) // Simulate network - - if (isValidCredentials(email, password)) { - val user = User( - id = "user_${System.currentTimeMillis()}", - email = email, - name = extractNameFromEmail(email) - ) - - saveUserSession(user) - - withContext(Dispatchers.Main) { - _currentUser.value = user - _isAuthenticated.value = true - } - } else { - _error.postValue("Invalid email or password") - } - } catch (e: Exception) { - Log.e(TAG, "Login error", e) - _error.postValue("Login failed: ${e.message}") - } finally { - _isLoading.postValue(false) - } - } - } - - fun logout(): Job { - checkInitialized() - _isLoading.postValue(true) - - return sessionScope.launch { - try { - clearUserSession() - withContext(Dispatchers.Main) { - _currentUser.value = null - _isAuthenticated.value = false - } - } catch (e: Exception) { - Log.e(TAG, "Logout error", e) - } finally { - _isLoading.postValue(false) - } - } - } - - fun isAuthenticatedSync(): Boolean = _isAuthenticated.value ?: false - - fun getCurrentUserSync(): User? = _currentUser.value - - fun clearError() { - _error.value = null - } - - // ==================== PRIVATE HELPERS ==================== - - private fun isValidCredentials(email: String, password: String): Boolean { - return email.isNotBlank() && - android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches() && - password.length >= 6 - } - - private fun extractNameFromEmail(email: String): String { - return email.substringBefore("@") - .split(".", "_", "-") - .joinToString(" ") { it.replaceFirstChar { c -> c.uppercase() } } - } - - private fun loadSavedSession() { - sessionScope.launch { - try { - val user = repository.getUser() - withContext(Dispatchers.Main) { - if (user != null) { - Log.d(TAG, "Found saved session for: ${user.email}") - _currentUser.value = user - _isAuthenticated.value = true - } else { - Log.d(TAG, "No saved session found") - _isAuthenticated.value = false - } - } - } catch (e: Exception) { - Log.e(TAG, "Failed to load saved user", e) - withContext(Dispatchers.Main) { - _isAuthenticated.value = false - } - clearUserSession() - } finally { - withContext(Dispatchers.Main) { - _isSessionChecked.value = true - } - } - } - } - - private fun saveUserSession(user: User) { - sessionScope.launch { - try { - repository.saveUser(user) - } catch (e: Exception) { - Log.e(TAG, "Error saving session", e) - } - } - } - - private fun clearUserSession() { - sessionScope.launch { - try { - repository.clearSession() - } catch (e: Exception) { - Log.e(TAG, "Error clearing session", e) - } - } - } - - fun cleanup() { - sessionScope.cancel() - } -} diff --git a/app/src/main/java/com/rho/studio/ui/core/model/ServiceModule.kt b/app/src/main/java/com/rho/studio/ui/core/model/ServiceModule.kt deleted file mode 100644 index 5e64b8e..0000000 --- a/app/src/main/java/com/rho/studio/ui/core/model/ServiceModule.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.rho.studio.ui.core.model - -import androidx.annotation.ColorRes - -/** - * Represents a service or module available on the home dashboard. - * - * @property id Unique identifier for the service. - * @property titleRes String resource ID for the service name. - * @property backgroundColor Background color resource for the button. - */ -data class ServiceModule( - val id: String, - val titleRes: Int, - @ColorRes val backgroundColor: Int -) diff --git a/build.gradle.kts b/build.gradle.kts index 895ed18..8547f7a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,6 @@ plugins { alias(libs.plugins.android.application) apply false + alias(libs.plugins.android.library) apply false alias(libs.plugins.kotlin.compose) apply false alias(libs.plugins.kotlin.parcelize) apply false } \ No newline at end of file diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts new file mode 100644 index 0000000..9dc8ef2 --- /dev/null +++ b/core/data/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + alias(libs.plugins.android.library) +} + +android { + namespace = "com.rho.studio.ui.core.data" + compileSdk = 37 + + defaultConfig { + minSdk = 23 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } +} + +dependencies { + implementation(project(path = ":core:domain")) + implementation(libs.androidx.core.ktx) + implementation(libs.gson) + implementation(libs.androidx.lifecycle.runtime.ktx) +} 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 new file mode 100644 index 0000000..bfdde4c --- /dev/null +++ b/core/data/src/main/java/com/rho/studio/ui/core/data/manager/SessionManager.kt @@ -0,0 +1,167 @@ +/** + * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗ + * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗ + * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝ + * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ + * + * ============================================================================================== + * File: SessionManager.kt + * Author: Alexis Tercero + * Email: alexis.tercero@rho.studio + * Date: 2026-08-04 + * ============================================================================================== + * 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. + * ============================================================================================== + */ +package com.rho.studio.ui.core.data.manager + +import android.content.Context +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.usecase.SessionManagerInterface +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** + * # SessionManager + * Implementation of SessionManagerInterface and SSOT for session state. + */ +class SessionManager private constructor() : SessionManagerInterface { + + companion object { + + /** 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 + + /** 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 + + this.repository = repository + isInitialized = true + 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 + saveUserSession(user) + } + + override fun clearSession() { + checkInitialized() + _currentUser.value = null + _isAuthenticated.value = false + clearUserSession() + } + + override fun extractNameFromEmail(email: String): String { + return email.substringBefore("@") + .split(".", "_", "-") + .joinToString(" ") { it.replaceFirstChar { c -> c.uppercase() } } + } + + private fun loadSavedSession() { + sessionScope.launch { + try { + val user = repository.getUser() // persistence + if (user != null) { + _currentUser.value = user + _isAuthenticated.value = true + } else { + _isAuthenticated.value = false + } + } catch (e: Exception) { + _isAuthenticated.value = false + clearUserSession() + } finally { + _isSessionChecked.value = true + } + } + } + + private fun saveUserSession(user: User) { + sessionScope.launch { + try { + repository.saveUser(user) + } catch (e: Exception) { + } + } + } + + private fun clearUserSession() { + sessionScope.launch { + try { + repository.clearSession() + } catch (e: Exception) { + } + } + } + + fun isAuthenticatedSync(): Boolean = _isAuthenticated.value + fun getCurrentUserSync(): User? = _currentUser.value + fun clearError() { _error.value = null } + fun cleanup() { sessionScope.cancel() } +} 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 new file mode 100644 index 0000000..3d891da --- /dev/null +++ b/core/data/src/main/java/com/rho/studio/ui/core/data/repository/AuthRepositoryImpl.kt @@ -0,0 +1,52 @@ +/** + * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗ + * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗ + * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝ + * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ + * + * ============================================================================================== + * File: AuthRepositoryImpl.kt + * Author: Alexis Tercero + * Email: alexis.tercero@rho.studio + * Date: 2026-08-04 + * ============================================================================================== + * Description: Repository for persisting session data. Current as a mock. + * ============================================================================================== + */ +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 + +/** + * Mock implementation of [AuthRepository] for development purposes. + * This will be replaced by Firebase Auth in the future. + */ +class AuthRepositoryImpl : 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() } } + } +} diff --git a/app/src/main/java/com/rho/studio/ui/core/repository/SessionRepository.kt b/core/data/src/main/java/com/rho/studio/ui/core/data/repository/SessionRepository.kt similarity index 96% rename from app/src/main/java/com/rho/studio/ui/core/repository/SessionRepository.kt rename to core/data/src/main/java/com/rho/studio/ui/core/data/repository/SessionRepository.kt index 3cc118e..e774881 100644 --- a/app/src/main/java/com/rho/studio/ui/core/repository/SessionRepository.kt +++ b/core/data/src/main/java/com/rho/studio/ui/core/data/repository/SessionRepository.kt @@ -10,18 +10,18 @@ * File: SessionRepository.kt * Author: Alexis Tercero * Email: alexis.tercero@rho.studio - * Date: 2026-07-14 + * Date: 2026-08-04 * ============================================================================================== * Description: Repository for persisting session data. * ============================================================================================== */ -package com.rho.studio.ui.core.repository +package com.rho.studio.ui.core.data.repository import android.content.Context import android.content.SharedPreferences import androidx.core.content.edit import com.google.gson.Gson -import com.rho.studio.ui.core.model.User +import com.rho.studio.ui.core.domain.model.User /** * Interface defining the persistence operations for user sessions. diff --git a/core/domain/build.gradle.kts b/core/domain/build.gradle.kts new file mode 100644 index 0000000..743a784 --- /dev/null +++ b/core/domain/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + kotlin("jvm") +} + +dependencies { + implementation(libs.gson) + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") + testImplementation(libs.junit) + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") +} diff --git a/app/src/main/java/com/rho/studio/ui/core/model/Credentials.kt b/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/model/Credentials.kt similarity index 63% rename from app/src/main/java/com/rho/studio/ui/core/model/Credentials.kt rename to core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/model/Credentials.kt index 877d371..b7e81a2 100644 --- a/app/src/main/java/com/rho/studio/ui/core/model/Credentials.kt +++ b/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/model/Credentials.kt @@ -10,35 +10,27 @@ * File: Credentials.kt * Author: Alexis Tercero * Email: alexis.tercero@rho.studio - * Date: 2026-02-23 + * Date: 2026-08-05 * ============================================================================================== - * Description: + * Description: core:domain * Represents the user's authentication data and provides validation logic. * ============================================================================================== */ -package com.rho.studio.ui.core.model - -import android.util.Patterns +package com.rho.studio.ui.core.domain.model /** * Represents the user's authentication data and provides validation logic. - * - * This class encapsulates the email and password fields, offering helper properties - * to verify format integrity (RFC-compliant email patterns) and security requirements - * (minimum password length). - * - * It is primarily used by ViewModels to manage UI state and provide immediate - * validation feedback to the user. - * - * @property email The user's email address. Defaults to an empty string. - * @property password The user's password. Defaults to an empty string. */ 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() && Patterns.EMAIL_ADDRESS.matcher(email).matches() + get() = email.isNotBlank() && email.matches(EMAIL_REGEX) val isPasswordValid: Boolean get() = password.length >= 6 @@ -46,24 +38,8 @@ data class Credentials( val isValid: Boolean get() = isEmailValid && isPasswordValid - val validationErrors: List - get() { - val errors = mutableListOf() - if (email.isBlank()) errors.add("Email is required") - else if (!isEmailValid) errors.add("Invalid email format") - - if (password.isBlank()) errors.add("Password is required") - else if (!isPasswordValid) errors.add("Password must be at least 6 characters") - - return errors - } - - val isEmpty: Boolean - get() = email.isEmpty() && password.isEmpty() - fun clear() { email = "" password = "" } - -} \ No newline at end of file +} diff --git a/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/model/Result.kt b/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/model/Result.kt new file mode 100644 index 0000000..13fcf1b --- /dev/null +++ b/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/model/Result.kt @@ -0,0 +1,37 @@ +/** + * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗ + * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗ + * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝ + * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ + * + * ============================================================================================== + * File: Result.kt + * Author: Alexis Tercero + * Email: alexis.tercero@rho.studio + * Date: 2026-08-05 + * ============================================================================================== + * Description: core:domain + * A generic class that holds a value with its loading status. + * ============================================================================================== + */ +package com.rho.studio.ui.core.domain.model + +/** A generic class that holds a value with its loading status.*/ +sealed class Result { + data class Success(val data: T) : Result() + data class Error(val exception: Exception) : Result() + object Loading : Result() + + override fun toString(): String { + return when (this) { + is Success<*> -> "Success[data=$data]" + is Error -> "Error[exception=$exception]" + Loading -> "Loading" + } + } +} + +val Result<*>.succeeded + get() = this is Result.Success \ No newline at end of file diff --git a/app/src/main/java/com/rho/studio/ui/core/model/User.kt b/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/model/User.kt similarity index 73% rename from app/src/main/java/com/rho/studio/ui/core/model/User.kt rename to core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/model/User.kt index 1148ff4..38859c0 100644 --- a/app/src/main/java/com/rho/studio/ui/core/model/User.kt +++ b/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/model/User.kt @@ -10,15 +10,13 @@ * File: User.kt * Author: Alexis Tercero * Email: alexis.tercero@rho.studio - * Date: 2026-02-23 + * Date: 2026-08-05 * ============================================================================================== - * Description: User model - Core data class used across features + * Description: User model - core:domain data class used across features * ============================================================================================== */ -package com.rho.studio.ui.core.model +package com.rho.studio.ui.core.domain.model -import android.os.Parcelable -import kotlinx.parcelize.Parcelize import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -26,11 +24,11 @@ import java.util.Locale /** * User model - Core data class used across features * - * This class implements [Parcelable] via `@Parcelize` to allow efficient data - * transfer between Android components (Activities, Fragments) and to survive - * configuration changes (screen rotations). + * Designed to be passed between Compose destinations and stored within + * ViewModel states. It is structured to survive configuration changes + * through standard state restoration mechanisms. * - * It encapsulates basic identity data, profile information, and nested + * Encapsulates basic identity data, profile information, and nested * [UserPreferences]. * * @property id Unique identifier for the user. @@ -42,7 +40,6 @@ import java.util.Locale * @property preferences Nested settings and UI configurations. * @property isActive Flag indicating if the account is currently enabled. */ -@Parcelize data class User( val id: String, val email: String, @@ -52,7 +49,7 @@ data class User( val lastLogin: Long = System.currentTimeMillis(), val preferences: UserPreferences = UserPreferences(), val isActive: Boolean = true -) : Parcelable { +) { val initials: String get() = if (name.isNotEmpty()) { @@ -66,7 +63,7 @@ data class User( } val displayName: String - get() = if (name.isNotEmpty()) name else email.substringBefore("@") + get() = name.ifEmpty { email.substringBefore("@") } fun getFormattedCreatedAt(pattern: String = "MMM dd, yyyy"): String { return SimpleDateFormat(pattern, Locale.getDefault()).format(Date(createdAt)) @@ -85,30 +82,9 @@ data class User( } } } -/** -* Consider splitting if: -* UserPreferences grows significantly (adds 10+ properties) -* Other models depend on UserPreferences (creating circular dependencies) -* File exceeds 200-300 lines -* Different teams own User and UserPreferences -* */ -@Parcelize + data class UserPreferences( val darkMode: Boolean = false, val notificationsEnabled: Boolean = true, val language: String = "en" -) : Parcelable - -/** - * User model - Core data class used across features - * - * In Android, you cannot simply pass a custom Kotlin object - * (like User) directly from one Activity to another or - * save it when the screen rotates. - * The data must be converted into a format the Android - * System understands (a byte stream). - * This process is called Serialization. - * - * The standard way to do this in Android - * is by implementing the Parcelable interface. - */ \ No newline at end of file +) diff --git a/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/repository/AuthRepository.kt b/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/repository/AuthRepository.kt new file mode 100644 index 0000000..ecfff80 --- /dev/null +++ b/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/repository/AuthRepository.kt @@ -0,0 +1,34 @@ +/** + * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗ + * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗ + * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝ + * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ + * + * ============================================================================================== + * File: AuthRepository.kt + * Author: Alexis Tercero + * Email: alexis.tercero@rho.studio + * Date: 2026-08-04 + * ============================================================================================== + * Description: Expected behavior of the Auth feature. + * ============================================================================================== + */ +package com.rho.studio.ui.core.domain.repository + +import com.rho.studio.ui.core.domain.model.Credentials +import com.rho.studio.ui.core.domain.model.User + +/** + * Interface defining the authentication operations. + */ +interface AuthRepository { + /** + * Authenticates a user with the provided credentials. + * @param credentials The user's email and password. + * @return The authenticated [User]. + * @throws Exception if authentication fails. + */ + suspend fun login(credentials: Credentials): User +} diff --git a/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/usecase/BaseUseCase.kt b/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/usecase/BaseUseCase.kt new file mode 100644 index 0000000..ff7e56a --- /dev/null +++ b/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/usecase/BaseUseCase.kt @@ -0,0 +1,67 @@ +/** + * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗ + * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗ + * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝ + * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ + * + * ============================================================================================== + * File: BaseUseCase.kt + * Author: Alexis Tercero + * Email: alexis.tercero@rho.studio + * Date: 2026-08-05 + * ============================================================================================== + * Description: Architectural foundation for all business logic components (Use Cases/Interactors). + * Provides a standardized execution pattern for domain transactions, ensuring + * thread safety, consistent error handling, and result wrapping. + * ============================================================================================== + */ +package com.rho.studio.ui.core.domain.usecase + +import com.rho.studio.ui.core.domain.model.Result +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +/** + * # BaseUseCase + * + * Base class for all Use Cases in the Domain layer. A Use Case (or Interactor) represents + * a single business transaction or user action. + * + * ### Key Responsibilities: + * 1. **Thread Management**: Automatically offloads execution to a background dispatcher + * (defaults to [Dispatchers.IO]) using `withContext`. + * 2. **Standardized Execution**: Uses the `invoke` operator to allow Use Cases to be + * called as functions (e.g., `loginUseCase(credentials)`). + * 3. **Consistent Error Handling**: Wraps the execution in a `try-catch` block, catching + * any [Exception] and transforming it into a [Result.Error]. + * 4. **Result Wrapping**: Automatically wraps successful execution into a [Result.Success]. + * + * ### Generics: + * @param P The Input Parameter type required by the Use Case. Use `Unit` if no input is needed. + * @param R The Output Result type. Must be a non-nullable type ([Any]). + * + * ### Implementation: + * Subclasses must implement the [execute] method to define the specific business logic. + * + * @property coroutineDispatcher The dispatcher where the logic will run. Defaults to [Dispatchers.IO]. + */ +abstract class BaseUseCase(private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO) { + + suspend operator fun invoke(parameters: P): Result { + return try { + withContext(coroutineDispatcher) { + execute(parameters).let { + Result.Success(it) + } + } + } catch (e: Exception) { + Result.Error(e) + } + } + + @Throws(RuntimeException::class) + protected abstract suspend fun execute(parameters: P): R +} 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 new file mode 100644 index 0000000..bb2af5e --- /dev/null +++ b/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/usecase/LoginUseCase.kt @@ -0,0 +1,46 @@ +/** + * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗ + * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗ + * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝ + * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ + * + * ============================================================================================== + * File: LoginUseCase.kt + * Author: Alexis Tercero + * Email: alexis.tercero@rho.studio + * Date: 2026-08-06 + * ============================================================================================== + * Description: Orchestrates the authentication process by validating user credentials, + * interacting with the AuthRepository to verify identity, and updating the + * global application session state upon successful login. + * ============================================================================================== + */ +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 + +/*** Encapsulates the login business transaction.*/ +class LoginUseCase( + 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 + val user = authRepository.login(parameters) + + // 3. 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 new file mode 100644 index 0000000..0005cf1 --- /dev/null +++ b/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/usecase/LogoutUseCase.kt @@ -0,0 +1,31 @@ +/** + * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗ + * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗ + * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝ + * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ + * + * ============================================================================================== + * File: LogoutUseCase.kt + * Author: Alexis Tercero + * Email: alexis.tercero@rho.studio + * Date: 2026-08-06 + * ============================================================================================== + * Description: Handles the termination of the user session by clearing authentication tokens, + * resetting global application state, and ensuring secure cleanup of + * persisted identity data. + * ============================================================================================== + */ +package com.rho.studio.ui.core.domain.usecase + +/*** Encapsulates the logout business transaction.*/ +class LogoutUseCase( + private val sessionManager: SessionManagerInterface +) : BaseUseCase() { + + override suspend fun execute(parameters: Unit) { + // Atomic cleanup of session state + sessionManager.clearSession() + } +} diff --git a/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/usecase/SessionManagerInterface.kt b/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/usecase/SessionManagerInterface.kt new file mode 100644 index 0000000..425b99c --- /dev/null +++ b/core/domain/src/main/kotlin/com/rho/studio/ui/core/domain/usecase/SessionManagerInterface.kt @@ -0,0 +1,26 @@ +/** + * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗ + * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗ + * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝ + * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ + * + * ============================================================================================== + * File: SessionManagerInterface.kt + * Author: Alexis Tercero + * Email: alexis.tercero@rho.studio + * Date: 2026-08-04 + * ============================================================================================== + * Description: SessionManager expected behavior . + * ============================================================================================== + */ +package com.rho.studio.ui.core.domain.usecase + +import com.rho.studio.ui.core.domain.model.User + +interface SessionManagerInterface { + fun updateSession(user: User) + fun clearSession() + fun extractNameFromEmail(email: String): String +} 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 new file mode 100644 index 0000000..5e64b57 --- /dev/null +++ b/core/domain/src/test/kotlin/com/rho/studio/ui/core/domain/usecase/LoginUseCaseTest.kt @@ -0,0 +1,85 @@ +/** + * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗ + * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗ + * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝ + * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ + * + * ============================================================================================== + * File: LoginUseCaseTest.kt + * Author: Alexis Tercero + * Email: alexis.tercero@rho.studio + * Date: 2026-08-06 + * ============================================================================================== + * Description: Check expected behavior of [LoginUseCase] + * ============================================================================================== + */ +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.repository.AuthRepository +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +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") + } + } + loginUseCase = LoginUseCase(authRepository, sessionManager) + } + + @Test + fun `login success with valid credentials`() = runBlocking { + val credentials = Credentials("test@rho.studio", "password123") + 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) + } + + @Test + fun `login failure with invalid credentials`() = runBlocking { + val credentials = Credentials("invalid-email", "short") + val result = loginUseCase(credentials) + assertTrue(result is Result.Error) + assertTrue((result as Result.Error).exception is IllegalArgumentException) + } + + @Test + fun `logout success`() = runBlocking { + val logoutUseCase = LogoutUseCase(sessionManager) + logoutUseCase(Unit) + assertTrue(isSessionCleared) + } +} diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts new file mode 100644 index 0000000..261b1c8 --- /dev/null +++ b/core/ui/build.gradle.kts @@ -0,0 +1,36 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "com.rho.studio.ui.core.ui" + compileSdk = 37 + + defaultConfig { + minSdk = 23 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildFeatures { + compose = true + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } +} + +dependencies { + implementation(project(path = ":core:domain")) + implementation(project(path = ":core:data")) + + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.lifecycle.viewmodel.compose) +} diff --git a/app/src/main/java/com/rho/studio/ui/core/base/BaseViewModel.kt b/core/ui/src/main/java/com/rho/studio/ui/core/ui/base/BaseViewModel.kt similarity index 67% rename from app/src/main/java/com/rho/studio/ui/core/base/BaseViewModel.kt rename to core/ui/src/main/java/com/rho/studio/ui/core/ui/base/BaseViewModel.kt index 04175eb..ec657ac 100644 --- a/app/src/main/java/com/rho/studio/ui/core/base/BaseViewModel.kt +++ b/core/ui/src/main/java/com/rho/studio/ui/core/ui/base/BaseViewModel.kt @@ -10,7 +10,7 @@ * File: BaseViewModel.kt * Author: Alexis Tercero * Email: alexis.tercero@rho.studio - * Date: 2026-07-15 + * Date: 2026-08-05 * ============================================================================================== * Description: * The BaseViewModel is an abstract base class designed for the Rho Studio UI architecture. @@ -19,20 +19,21 @@ * all feature-specific ViewModels. * * Key Features - * •Automatic Loading State: Integrated tracking of background tasks. - * •Safe Coroutine Execution: Built-in exception handling to prevent app crashes. - * •Job Management: Automatic cancellation of active coroutines when the ViewModel is cleared. - * •UI Communication: Standardized LiveData streams for errors and toast notifications. + * • Automatic Loading State: Integrated tracking of background tasks via StateFlow. + * • Safe Coroutine Execution: Built-in exception handling wrappers to prevent app crashes. + * • Job Management: Tracking and automatic cancellation of active coroutines. + * • UI Communication: Standardized StateFlow streams for errors and toast notifications. * ============================================================================================== */ -package com.rho.studio.ui.core.base +package com.rho.studio.ui.core.ui.base -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch /** @@ -49,39 +50,35 @@ import kotlinx.coroutines.launch * * •Safe Coroutine Execution: Built-in exception handling to prevent app crashes. * - * •Job Management: Automatic cancellation of active coroutines when the ViewModel is cleared. + * •Job Management: Automatic tracking and cancellation of active coroutines when the ViewModel is cleared. * - * •UI Communication: Standardized LiveData streams for errors and toast notifications. + * •UI Communication: Standardized StateFlow streams for errors and toast notifications. * * In standard Android development, if a coroutine launched in viewModelScope - * throws an exception that isn't caught, the entire app crashes.By using launchSafe, - * avoiding repetitive boilerplate code. Instead of writing try-catch in every single function + * throws an exception that isn't caught, the entire app crashes. By using `launchSafe` + * or `launchWithLoading`, you avoid repetitive boilerplate code. Instead of writing + * try-catch in every single function, the base class handles exceptions globally + * via `handleError` while still allowing optional local error handling. */ abstract class BaseViewModel : ViewModel() { - // Loading state - private val _isLoading = MutableLiveData(false) - val isLoading: LiveData = _isLoading + private val _isLoading = MutableStateFlow(false) + val isLoading: StateFlow = _isLoading.asStateFlow() - // Error messages - private val _error = MutableLiveData() - val error: LiveData = _error + private val _error = MutableStateFlow(null) + val error: StateFlow = _error.asStateFlow() - // Toast messages (one-time) - private val _toastMessage = MutableLiveData() - val toastMessage: LiveData = _toastMessage + private val _toastMessage = MutableStateFlow(null) + val toastMessage: StateFlow = _toastMessage.asStateFlow() - // Track active jobs private val jobs = mutableListOf() - /** - * Launch a coroutine with automatic loading state - */ + /** Launch a coroutine with automatic loading state*/ protected fun launchWithLoading( block: suspend CoroutineScope.() -> Unit, onError: ((Exception) -> Unit)? = null ): Job { - _isLoading.postValue(true) + _isLoading.value = true val job = viewModelScope.launch { try { @@ -90,7 +87,7 @@ abstract class BaseViewModel : ViewModel() { handleError(e) onError?.invoke(e) } finally { - _isLoading.postValue(false) + _isLoading.value = false } } @@ -98,15 +95,6 @@ abstract class BaseViewModel : ViewModel() { return job } - /** - * Executes a coroutine block safely within the [viewModelScope]. - * - * This function wraps the execution of the [block] in a try-catch block. - * If an exception occurs during the execution of the coroutine, it is caught - * and passed to [handleError], preventing the app from crashing. - * - * @param block The suspendable lambda expression to be executed. - */ protected fun launchSafe( block: suspend CoroutineScope.() -> Unit, onError: ((Exception) -> Unit)? = null @@ -136,36 +124,27 @@ abstract class BaseViewModel : ViewModel() { */ protected open fun handleError(e: Exception) { e.printStackTrace() - _error.postValue(e.message ?: "An error occurred") + _error.value = e.message ?: "An error occurred" } - /** Clear current error message */ fun clearError() { _error.value = null } - // ==================== TOAST MESSAGES ==================== - - /** Show a one-time toast message to user */ protected fun showToast(message: String) { - _toastMessage.postValue(message) + _toastMessage.value = message } - /** Clear current toast message */ fun clearToastMessage() { _toastMessage.value = null } - // ==================== JOB MANAGEMENT ==================== - - /** Cancel all active coroutine jobs */ fun cancelAllJobs() { jobs.forEach { it.cancel() } jobs.clear() } override fun onCleared() { - cancelAllJobs() // Prevent memory leaks - //super.onCleared() + cancelAllJobs() } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/rho/studio/ui/features/common/HeaderViewModel.kt b/core/ui/src/main/java/com/rho/studio/ui/core/ui/common/HeaderViewModel.kt similarity index 71% rename from app/src/main/java/com/rho/studio/ui/features/common/HeaderViewModel.kt rename to core/ui/src/main/java/com/rho/studio/ui/core/ui/common/HeaderViewModel.kt index a935143..2aa66e7 100644 --- a/app/src/main/java/com/rho/studio/ui/features/common/HeaderViewModel.kt +++ b/core/ui/src/main/java/com/rho/studio/ui/core/ui/common/HeaderViewModel.kt @@ -10,32 +10,28 @@ * File: HeaderViewModel.kt * Author: Alexis Tercero * Email: alexis.tercero@rho.studio - * Date: 2026-07-21 + * Date: 2026-08-06 * ========================================================================== * Description: * ViewModel for the reusable PageHeaderFragment. * Decouples the header from feature-specific ViewModels by sourcing - * user data directly from the SessionManager. + * user data directly from the SessionManager and managing the + * page-specific title state reactively. * ========================================================================== */ -package com.rho.studio.ui.features.common +package com.rho.studio.ui.core.ui.common -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import com.rho.studio.ui.core.base.BaseViewModel -import com.rho.studio.ui.core.manager.SessionManager -import com.rho.studio.ui.core.model.User +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 kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow class HeaderViewModel : BaseViewModel() { - private val sessionManager = SessionManager.getInstance() - - val currentUser: LiveData = sessionManager.currentUser - - private val _title = MutableLiveData() - val title: LiveData = _title - - fun setTitle(newTitle: String) { - _title.value = newTitle - } -} \ No newline at end of file + val currentUser: StateFlow = sessionManager.currentUser + private val _title = MutableStateFlow("") + val title: StateFlow = _title.asStateFlow() + fun setTitle(newTitle: String) { _title.value = newTitle } +} diff --git a/app/src/main/java/com/rho/studio/ui/features/common/PageFooter.kt b/core/ui/src/main/java/com/rho/studio/ui/core/ui/common/PageFooter.kt similarity index 69% rename from app/src/main/java/com/rho/studio/ui/features/common/PageFooter.kt rename to core/ui/src/main/java/com/rho/studio/ui/core/ui/common/PageFooter.kt index 5cc1b81..03da115 100644 --- a/app/src/main/java/com/rho/studio/ui/features/common/PageFooter.kt +++ b/core/ui/src/main/java/com/rho/studio/ui/core/ui/common/PageFooter.kt @@ -10,24 +10,28 @@ * File: PageFooter.kt (composable UI) * Author: Alexis Tercero * Email: alexis.tercero@rho.studio - * Date: 2026-07-29 + * Date: 2026-08-06 * ============================================================================ - * Description: - * A persistent UI component placed at the bottom of the screen to - * provide access to global session-level actions. + * Description: + * A reusable footer component designed for consistent placement at the + * bottom of screens to provide access to essential session-level + * actions. It ensures a uniform user experience across different + * modules by standardizing the appearance and behavior of the + * logout mechanism. * * Key Features: - * • Session Management: Provides a clear entry point for the user - * to log out, delegating the operation to the HomeViewModel. - * • Distinct Styling: Utilizes the brand's primary red (rho_red) - * for the logout action to signal its significance. - * • Layout Integration: Designed to span the full width of the - * screen with standard padding, ensuring high touch-target visibility. + * • Session Management: Provides a clear logout entry point, + * delegating the action to the caller via a callback. + * • Semantic Styling: Utilizes the brand's primary red (rho_red) + * to visually communicate the destructive nature of the action. + * • Adaptive Layout: Configured to span the full width with + * standard padding for high touch-target visibility. * ============================================================================ */ -package com.rho.studio.ui.features.common +package com.rho.studio.ui.core.ui.common import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Text @@ -37,17 +41,17 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import com.rho.studio.ui.R -import com.rho.studio.ui.features.home.HomeViewModel +import com.rho.studio.ui.core.ui.R @Composable fun PageFooter( - viewModel: HomeViewModel, + onLogoutClick: () -> Unit, modifier: Modifier = Modifier ) { TextButton( - onClick = { viewModel.logout() }, + onClick = onLogoutClick, modifier = modifier + .navigationBarsPadding() .fillMaxWidth() .padding(16.dp), colors = ButtonDefaults.textButtonColors( diff --git a/app/src/main/java/com/rho/studio/ui/features/common/PageHeader.kt b/core/ui/src/main/java/com/rho/studio/ui/core/ui/common/PageHeader.kt similarity index 65% rename from app/src/main/java/com/rho/studio/ui/features/common/PageHeader.kt rename to core/ui/src/main/java/com/rho/studio/ui/core/ui/common/PageHeader.kt index e27dead..821a05a 100644 --- a/app/src/main/java/com/rho/studio/ui/features/common/PageHeader.kt +++ b/core/ui/src/main/java/com/rho/studio/ui/core/ui/common/PageHeader.kt @@ -10,50 +10,56 @@ * File: PageHeader.kt (composable UI) * Author: Alexis Tercero * Email: alexis.tercero@rho.studio - * Date: 2026-07-29 + * Date: 2026-08-06 * ============================================================================ - * Description: - * A standard UI component that provides context and a personalized - * greeting at the top of the application's screens. + * Description: + * A standardized header component that serves as the primary entry point + * for user orientation within the application's layout. * * Key Features: - * • Personalized Greeting: Dynamically displays the current user's - * name, observing state from the HeaderViewModel. - * • Contextual Title: Provides a secondary text line to indicate - * the current section or active feature of the app. - * • Branding Styles: Applies consistent typography (24sp Bold) - * and the signature SilverGray color palette for readability. - * • Resource Integration: Uses localized string resources for - * formatted greetings (e.g., "Welcome, [User]"). + * • Reactive State Management: Leverages Kotlin StateFlow and + * collectAsState() to synchronize UI with HeaderViewModel + * session data in real-time. + * • Adaptive Greeting: Renders a personalized welcome message for + * authenticated users with a graceful fallback for guest states. + * • Hierarchical Typography: Establishes visual hierarchy using + * bold 24sp headlines and 16sp subtitles for clear section + * identification. + * • Rho Design System Compliance: Implements the SilverGray color + * specification and standard 16dp structural padding. + * • Localization Ready: Leverages Android's string resource system + * for multi-language greeting support. * ============================================================================ */ -package com.rho.studio.ui.features.common +package com.rho.studio.ui.core.ui.common import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.rho.studio.ui.R +import com.rho.studio.ui.core.ui.R @Composable fun PageHeader( viewModel: HeaderViewModel, modifier: Modifier = Modifier ) { - val currentUser by viewModel.currentUser.observeAsState() - val title by viewModel.title.observeAsState("") + val currentUser by viewModel.currentUser.collectAsState() + val title by viewModel.title.collectAsState() Column( - modifier = modifier.padding(16.dp) + modifier = modifier + .statusBarsPadding() + .padding(16.dp) ) { Text( text = stringResource(id = R.string.welcome_user, currentUser?.name ?: "User"), diff --git a/app/src/main/java/com/rho/studio/ui/ui/theme/Color.kt b/core/ui/src/main/java/com/rho/studio/ui/core/ui/theme/Color.kt similarity index 93% rename from app/src/main/java/com/rho/studio/ui/ui/theme/Color.kt rename to core/ui/src/main/java/com/rho/studio/ui/core/ui/theme/Color.kt index f35f95d..64955af 100644 --- a/app/src/main/java/com/rho/studio/ui/ui/theme/Color.kt +++ b/core/ui/src/main/java/com/rho/studio/ui/core/ui/theme/Color.kt @@ -1,4 +1,4 @@ -package com.rho.studio.ui.ui.theme +package com.rho.studio.ui.core.ui.theme import androidx.compose.ui.graphics.Color diff --git a/app/src/main/java/com/rho/studio/ui/ui/theme/Theme.kt b/core/ui/src/main/java/com/rho/studio/ui/core/ui/theme/Theme.kt similarity index 76% rename from app/src/main/java/com/rho/studio/ui/ui/theme/Theme.kt rename to core/ui/src/main/java/com/rho/studio/ui/core/ui/theme/Theme.kt index 8b629a3..1ade494 100644 --- a/app/src/main/java/com/rho/studio/ui/ui/theme/Theme.kt +++ b/core/ui/src/main/java/com/rho/studio/ui/core/ui/theme/Theme.kt @@ -1,6 +1,5 @@ -package com.rho.studio.ui.ui.theme +package com.rho.studio.ui.core.ui.theme -import android.app.Activity import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme @@ -21,22 +20,11 @@ private val LightColorScheme = lightColorScheme( primary = Purple40, secondary = PurpleGrey40, tertiary = Pink40 - - /* Other default colors to override - background = Color(0xFFFFFBFE), - surface = Color(0xFFFFFBFE), - onPrimary = Color.White, - onSecondary = Color.White, - onTertiary = Color.White, - onBackground = Color(0xFF1C1B1F), - onSurface = Color(0xFF1C1B1F), - */ ) @Composable fun UITheme( darkTheme: Boolean = isSystemInDarkTheme(), - // Dynamic color is available on Android 12+ dynamicColor: Boolean = true, content: @Composable () -> Unit ) { @@ -55,4 +43,4 @@ fun UITheme( typography = Typography, content = content ) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/rho/studio/ui/ui/theme/Type.kt b/core/ui/src/main/java/com/rho/studio/ui/core/ui/theme/Type.kt similarity index 50% rename from app/src/main/java/com/rho/studio/ui/ui/theme/Type.kt rename to core/ui/src/main/java/com/rho/studio/ui/core/ui/theme/Type.kt index c1b2fdd..1ad8bae 100644 --- a/app/src/main/java/com/rho/studio/ui/ui/theme/Type.kt +++ b/core/ui/src/main/java/com/rho/studio/ui/core/ui/theme/Type.kt @@ -1,4 +1,4 @@ -package com.rho.studio.ui.ui.theme +package com.rho.studio.ui.core.ui.theme import androidx.compose.material3.Typography import androidx.compose.ui.text.TextStyle @@ -15,20 +15,4 @@ val Typography = Typography( lineHeight = 24.sp, letterSpacing = 0.5.sp ) - /* Other default text styles to override - titleLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 22.sp, - lineHeight = 28.sp, - letterSpacing = 0.sp - ), - labelSmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 11.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ) - */ -) \ No newline at end of file +) diff --git a/app/src/main/res/values/colors.xml b/core/ui/src/main/res/values/colors.xml similarity index 98% rename from app/src/main/res/values/colors.xml rename to core/ui/src/main/res/values/colors.xml index f767871..a1098a9 100644 --- a/app/src/main/res/values/colors.xml +++ b/core/ui/src/main/res/values/colors.xml @@ -15,4 +15,4 @@ #FF0000 #4CAF50 #C0C0C0 - \ No newline at end of file + diff --git a/app/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml similarity index 82% rename from app/src/main/res/values/strings.xml rename to core/ui/src/main/res/values/strings.xml index d118a9e..1fde0fe 100644 --- a/app/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -1,9 +1,6 @@ - Enter your password - Enter your Email ID - Rho.Studio app - Hello blank fragment RhoStudio UI + Rho.Studio app Login Email address Password @@ -19,4 +16,4 @@ Sales Customers Reports - \ No newline at end of file + diff --git a/features/auth/build.gradle.kts b/features/auth/build.gradle.kts new file mode 100644 index 0000000..1137cb2 --- /dev/null +++ b/features/auth/build.gradle.kts @@ -0,0 +1,35 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "com.rho.studio.ui.features.auth" + compileSdk = 37 + + defaultConfig { + minSdk = 23 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildFeatures { + compose = true + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } +} + +dependencies { + implementation(project(path = ":core:domain")) + implementation(project(path = ":core:data")) + implementation(project(path = ":core:ui")) + + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.material3) + implementation(libs.androidx.compose.material.icons.extended) + implementation(libs.androidx.lifecycle.viewmodel.compose) +} diff --git a/app/src/main/java/com/rho/studio/ui/features/auth/LoginScreen.kt b/features/auth/src/main/java/com/rho/studio/ui/features/auth/LoginScreen.kt similarity index 72% rename from app/src/main/java/com/rho/studio/ui/features/auth/LoginScreen.kt rename to features/auth/src/main/java/com/rho/studio/ui/features/auth/LoginScreen.kt index b5a43bf..d5d88b1 100644 --- a/app/src/main/java/com/rho/studio/ui/features/auth/LoginScreen.kt +++ b/features/auth/src/main/java/com/rho/studio/ui/features/auth/LoginScreen.kt @@ -6,25 +6,18 @@ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ * - * ============================================================================ + * ============================================================================================== * File: LoginScreen.kt * Author: Alexis Tercero * Email: alexis.tercero@rho.studio - * Date: 2026-07-29 - * ============================================================================ - * Description: Implementation of the Login screen using Jetpack Compose and - * MVVM architecture. - * - * Features: - * - State Management: Utilizes LiveData observed as Compose State for reactive - * UI updates (e.g., loading states, input validation). - * - Unidirectional Data Flow (UDF): Events are passed from the UI to the - * ViewModel, while State flows down from the ViewModel to the Composables. - * - Component Modularization: Extracts input fields and buttons into dedicated - * sub-components for reusability and cleaner code structure. - * - Theming: Integrates custom branding colors (RhoRed, SilverGray) via - * gradient backgrounds and Material3 typography. - * ============================================================================ + * Date: 2026-08-06 + * ============================================================================================== + * Description: Provides the user interface for the authentication entry point. + * The screen renders a stylized login form including composable email and password + * input fields, handles input state via the LoginViewModel, and triggers + * identity verification workflows. It features a branded vertical gradient + * background and centers the authentication components for optimal focus. + * ============================================================================================== */ package com.rho.studio.ui.features.auth @@ -35,12 +28,10 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush @@ -49,33 +40,25 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.rho.studio.ui.R +import com.rho.studio.ui.core.ui.R import com.rho.studio.ui.features.auth.components.LoginButton import com.rho.studio.ui.features.auth.components.LoginEmailField import com.rho.studio.ui.features.auth.components.LoginPasswordField -import com.rho.studio.ui.ui.theme.Black -import com.rho.studio.ui.ui.theme.RhoRed -import com.rho.studio.ui.ui.theme.RhoStrongGray -import com.rho.studio.ui.ui.theme.SilverGray -import com.rho.studio.ui.ui.theme.White +import com.rho.studio.ui.core.ui.theme.Black +import com.rho.studio.ui.core.ui.theme.RhoRed +import com.rho.studio.ui.core.ui.theme.SilverGray @Composable fun LoginScreen( viewModel: LoginViewModel, modifier: Modifier = Modifier ) { - val isLoading by viewModel.isLoading.observeAsState(false) - Box( modifier = modifier .fillMaxSize() .background( brush = Brush.verticalGradient( - colors = listOf( - RhoRed, - SilverGray, - Black - ) + colors = listOf(RhoRed, SilverGray, Black) ) ) .padding(24.dp), diff --git a/app/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 similarity index 70% rename from app/src/main/java/com/rho/studio/ui/features/auth/LoginViewModel.kt rename to features/auth/src/main/java/com/rho/studio/ui/features/auth/LoginViewModel.kt index cfa794b..4995dc4 100644 --- a/app/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,18 +10,19 @@ * File: LoginViewModel.kt * Author: Alexis Tercero * Email: alexis.tercero@rho.studio - * Date: 2026-07-29 + * Date: 2026-08-06 * ============================================================================ * Description: * The LoginViewModel manages the state and business logic for the - * Authentication screen in a pure Jetpack Compose environment. - * It leverages the Rho Studio BaseViewModel architecture to handle - * user input validation, asynchronous login requests via SessionManager, - * and reactive UI states. + * 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.base.BaseViewModel - * •Dependencies: - * •SessionManager: Singleton handling network/local session state. + * •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 @@ -38,11 +39,11 @@ * against concurrent attempts using the base loading state. * •Execution: performLogin() utilizes launchWithLoading() to * automatically manage the UI loading state and error trapping. - * •Network: Calls sessionManager.login(). + * •UseCase: Executes loginUseCase(credentials) within a managed coroutine. * •Result Handling: - * •Success: Sets success toast and relies on SessionManager state. + * •Success: Sets success toast; navigation is handled via SessionManager state. * •Failure: Customizes error messages via the handleError() hook. - * Lifecycle & Architecture + * Layering & Architecture * •Job Management: * Relies on BaseViewModel's automated job tracking and cleanup * to prevent memory leaks without manual cancellation logic. @@ -56,19 +57,24 @@ package com.rho.studio.ui.features.auth import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope -import com.rho.studio.ui.core.base.BaseViewModel -import com.rho.studio.ui.core.manager.SessionManager -import com.rho.studio.ui.core.model.Credentials +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 kotlin.time.Duration.Companion.milliseconds class LoginViewModel : BaseViewModel() { // ==================== DEPENDENCIES ==================== - private val sessionManager = SessionManager.getInstance() + private val loginUseCase = LoginUseCase(AuthRepositoryImpl(), SessionManager.getInstance()) private var loginJob: Job? = null private var emailDebounceJob: Job? = null private var passwordDebounceJob: Job? = null @@ -79,12 +85,13 @@ class LoginViewModel : BaseViewModel() { private set val credentials = Credentials() // ==================== UI STATE ==================== - private val _emailError = MutableLiveData() - val emailError: LiveData = _emailError - private val _passwordError = MutableLiveData() - val passwordError: LiveData = _passwordError - private val _isFormValid = MutableLiveData(false) - val isFormValid: LiveData = _isFormValid + 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 ==================== fun onEmailChanged(email: String) { this.email = email @@ -92,7 +99,7 @@ class LoginViewModel : BaseViewModel() { emailDebounceJob?.cancel() emailDebounceJob = viewModelScope.launch { - delay(300) + delay(300.milliseconds) validateEmail() validateForm() } @@ -104,7 +111,7 @@ class LoginViewModel : BaseViewModel() { passwordDebounceJob?.cancel() passwordDebounceJob = viewModelScope.launch { - delay(300) + delay(300.milliseconds) validatePassword() validateForm() } @@ -130,45 +137,33 @@ class LoginViewModel : BaseViewModel() { _isFormValid.value = credentials.isValid } - // ==================== ACTIONS ==================== fun onLoginClick() { - // Guard against multiple concurrent login attempts - if (isLoading.value == true) return - + if (isLoading.value) return if (!credentials.isValid) { validateEmail() validatePassword() - showToast("Please fix the errors above") return } - performLogin() } private fun performLogin() { loginJob = launchWithLoading( block = { - // Delegate authentication to the session manager - sessionManager.login(credentials.email, credentials.password).join() - - // Evaluate the authentication result - if (sessionManager.isAuthenticatedSync()) { - showToast("Login successful!") - clearError() - } else { - // Capture and handle the specific error from SessionManager - val errorMsg = sessionManager.error.value ?: "Authentication failed" - handleError(Exception(errorMsg)) + when (val result = loginUseCase(credentials)) { + is Result.Success -> { + showToast("Login successful!") + clearError() + } + is Result.Error -> { + handleError(result.exception) + } + Result.Loading -> {} } - }, - onError = { - // General fallback if the login process crashes - showToast("Login process encountered an error. Please try again.") } ) } - // ==================== UTILITY METHODS ==================== fun resetForm() { email = "" password = "" @@ -181,11 +176,8 @@ class LoginViewModel : BaseViewModel() { } override fun handleError(e: Exception) { - // Ensure error messages are properly prefixed for context val message = e.message ?: "An unknown error occurred" - val formattedMessage = message.takeIf { it.startsWith("Login failed") } - ?: "Login failed: $message" - + val formattedMessage = if (message.startsWith("Login failed")) message else "Login failed: $message" super.handleError(Exception(formattedMessage, e.cause)) } } \ No newline at end of file diff --git a/app/src/main/java/com/rho/studio/ui/features/auth/components/LoginButton.kt b/features/auth/src/main/java/com/rho/studio/ui/features/auth/components/LoginButton.kt similarity index 76% rename from app/src/main/java/com/rho/studio/ui/features/auth/components/LoginButton.kt rename to features/auth/src/main/java/com/rho/studio/ui/features/auth/components/LoginButton.kt index c715b8a..850584c 100644 --- a/app/src/main/java/com/rho/studio/ui/features/auth/components/LoginButton.kt +++ b/features/auth/src/main/java/com/rho/studio/ui/features/auth/components/LoginButton.kt @@ -6,43 +6,40 @@ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ * - * ============================================================================ - * File: LoginButton.kt (composable UI) + * ============================================================================================== + * File: LoginButton.kt * Author: Alexis Tercero * Email: alexis.tercero@rho.studio - * Date: 2026-07-29 - * ============================================================================ - * Description: A custom Jetpack Compose button component for the Login screen. - * It observes the LoginViewModel state to handle validation logic - * and loading states, automatically disabling interaction and - * updating its UI when a login attempt is in progress. - * ============================================================================ + * Date: 2026-08-06 + * ============================================================================================== + * Description: A compose action button for triggering the authentication process. It + * synchronizes with LoginViewModel to handle loading states and form validation. + * ============================================================================================== */ package com.rho.studio.ui.features.auth.components -import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import com.rho.studio.ui.R +import com.rho.studio.ui.core.ui.R import com.rho.studio.ui.features.auth.LoginViewModel -import com.rho.studio.ui.ui.theme.RhoRed +import com.rho.studio.ui.core.ui.theme.RhoRed @Composable fun LoginButton( viewModel: LoginViewModel, modifier: Modifier = Modifier ) { - val isFormValid by viewModel.isFormValid.observeAsState(false) - val isLoading by viewModel.isLoading.observeAsState(false) + val isFormValid by viewModel.isFormValid.collectAsState() + val isLoading by viewModel.isLoading.collectAsState() Button( onClick = { viewModel.onLoginClick() }, @@ -54,4 +51,4 @@ fun LoginButton( ) { Text(text = stringResource(if (isLoading) R.string.rho_studio_app else R.string.login)) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/rho/studio/ui/features/auth/components/LoginEmailField.kt b/features/auth/src/main/java/com/rho/studio/ui/features/auth/components/LoginEmailField.kt similarity index 77% rename from app/src/main/java/com/rho/studio/ui/features/auth/components/LoginEmailField.kt rename to features/auth/src/main/java/com/rho/studio/ui/features/auth/components/LoginEmailField.kt index f7058f9..5ed6544 100644 --- a/app/src/main/java/com/rho/studio/ui/features/auth/components/LoginEmailField.kt +++ b/features/auth/src/main/java/com/rho/studio/ui/features/auth/components/LoginEmailField.kt @@ -6,16 +6,15 @@ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ * - * ============================================================================ - * File: LoginEmailField.kt (composable UI) + * ============================================================================================== + * File: LoginEmailField.kt * Author: Alexis Tercero * Email: alexis.tercero@rho.studio - * Date: 2026-07-29 - * ============================================================================ - * Description: A reusable Jetpack Compose component that provides a styled - * email input field for the Login screen, featuring validation - * state handling and integration with LoginViewModel. - * ============================================================================ + * Date: 2026-08-06 + * ============================================================================================== + * Description: A compose text input component for user email addresses, integrated with + * LoginViewModel for state management, validation feedback, and styling. + * ============================================================================================== */ package com.rho.studio.ui.features.auth.components @@ -28,34 +27,28 @@ import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType -import com.rho.studio.ui.R +import com.rho.studio.ui.core.ui.R import com.rho.studio.ui.features.auth.LoginViewModel -import com.rho.studio.ui.ui.theme.ErrorRed -import com.rho.studio.ui.ui.theme.RhoRed -import com.rho.studio.ui.ui.theme.RhoStrongGray -import com.rho.studio.ui.ui.theme.SilverGray -import com.rho.studio.ui.ui.theme.TitleGray +import com.rho.studio.ui.core.ui.theme.ErrorRed +import com.rho.studio.ui.core.ui.theme.RhoStrongGray +import com.rho.studio.ui.core.ui.theme.SilverGray +import com.rho.studio.ui.core.ui.theme.TitleGray @Composable fun LoginEmailField( viewModel: LoginViewModel, modifier: Modifier = Modifier ) { - val emailError by viewModel.emailError.observeAsState() + val emailError by viewModel.emailError.collectAsState() OutlinedTextField( value = viewModel.email, - onValueChange = { - viewModel.onEmailChanged(it) - }, + onValueChange = { viewModel.onEmailChanged(it) }, label = { Text(stringResource(R.string.email_hint)) }, modifier = modifier.fillMaxWidth(), isError = emailError != null, diff --git a/app/src/main/java/com/rho/studio/ui/features/auth/components/LoginPasswordField.kt b/features/auth/src/main/java/com/rho/studio/ui/features/auth/components/LoginPasswordField.kt similarity index 75% rename from app/src/main/java/com/rho/studio/ui/features/auth/components/LoginPasswordField.kt rename to features/auth/src/main/java/com/rho/studio/ui/features/auth/components/LoginPasswordField.kt index 1248616..e221d1c 100644 --- a/app/src/main/java/com/rho/studio/ui/features/auth/components/LoginPasswordField.kt +++ b/features/auth/src/main/java/com/rho/studio/ui/features/auth/components/LoginPasswordField.kt @@ -6,26 +6,15 @@ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ * - * ============================================================================ - * File: LoginPasswordField.kt (composable UI) + * ============================================================================================== + * File: LoginPasswordField.kt * Author: Alexis Tercero * Email: alexis.tercero@rho.studio - * Date: 2026-07-29 - * ============================================================================ - * Description: - * A specialized password input component for the Authentication screen. - * It integrates directly with the LoginViewModel to provide real-time - * validation feedback and visibility toggling. - * - * Key Features: - * • Reactive State: Observes password error states from the ViewModel. - * • Visibility Toggle: Built-in IconButton to switch between masked - * and plain text using VisualTransformation. - * • Standardized Styling: Uses the Rho Studio theme palette (RhoStrongGray, - * SilverGray, ErrorRed) for a consistent UI experience. - * • Accessibility: Includes localized hints and dynamic content descriptions - * for the visibility icons. - * ============================================================================ + * Date: 2026-08-06 + * ============================================================================================== + * Description: A compose password input component for the login screen that features + * visibility toggling, validation error handling, and standardized Rho styling. + * ============================================================================================== */ package com.rho.studio.ui.features.auth.components @@ -41,8 +30,8 @@ import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -51,27 +40,24 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation -import com.rho.studio.ui.R +import com.rho.studio.ui.core.ui.R import com.rho.studio.ui.features.auth.LoginViewModel -import com.rho.studio.ui.ui.theme.ErrorRed -import com.rho.studio.ui.ui.theme.RhoRed -import com.rho.studio.ui.ui.theme.RhoStrongGray -import com.rho.studio.ui.ui.theme.SilverGray -import com.rho.studio.ui.ui.theme.TitleGray +import com.rho.studio.ui.core.ui.theme.ErrorRed +import com.rho.studio.ui.core.ui.theme.RhoStrongGray +import com.rho.studio.ui.core.ui.theme.SilverGray +import com.rho.studio.ui.core.ui.theme.TitleGray @Composable fun LoginPasswordField( viewModel: LoginViewModel, modifier: Modifier = Modifier ) { - val passwordError by viewModel.passwordError.observeAsState() + val passwordError by viewModel.passwordError.collectAsState() var passwordVisible by remember { mutableStateOf(false) } OutlinedTextField( value = viewModel.password, - onValueChange = { - viewModel.onPasswordChanged(it) - }, + onValueChange = { viewModel.onPasswordChanged(it) }, label = { Text(stringResource(R.string.password_hint)) }, modifier = modifier.fillMaxWidth(), isError = passwordError != null, diff --git a/features/home/build.gradle.kts b/features/home/build.gradle.kts new file mode 100644 index 0000000..0fe76db --- /dev/null +++ b/features/home/build.gradle.kts @@ -0,0 +1,36 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "com.rho.studio.ui.features.home" + compileSdk = 37 + + defaultConfig { + minSdk = 23 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildFeatures { + compose = true + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } +} + +dependencies { + implementation(project(path = ":core:domain")) + implementation(project(path = ":core:data")) + implementation(project(path = ":core:ui")) + + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.material) + implementation(libs.androidx.material3) + implementation(libs.androidx.compose.material.icons.extended) + implementation(libs.androidx.lifecycle.viewmodel.compose) +} diff --git a/app/src/main/java/com/rho/studio/ui/features/home/HomeScreen.kt b/features/home/src/main/java/com/rho/studio/ui/features/home/HomeScreen.kt similarity index 81% rename from app/src/main/java/com/rho/studio/ui/features/home/HomeScreen.kt rename to features/home/src/main/java/com/rho/studio/ui/features/home/HomeScreen.kt index aff83ae..b5cd8a2 100644 --- a/app/src/main/java/com/rho/studio/ui/features/home/HomeScreen.kt +++ b/features/home/src/main/java/com/rho/studio/ui/features/home/HomeScreen.kt @@ -10,24 +10,26 @@ * File: HomeScreen.kt (composable UI) * Author: Alexis Tercero * Email: alexis.tercero@rho.studio - * Date: 2026-07-29 + * Date: 2026-08-06 * ============================================================================ - * Description: - * The primary landing screen of the application, serving as the main - * dashboard for user interactions. It orchestrates the display of + * Description: + * The primary landing screen of the application, serving as the main + * Rho Studio Home screen. It establishes the primary entry point and + * experience for user interactions. It orchestrates the display of * the header, available services, and the footer. * * Key Features: - * • Dynamic Background: Implements a signature vertical gradient - * (RhoRed to Black) defining the app's visual identity. - * • Multi-ViewModel Architecture: Coordinates state between + * • Dynamic Background: Implements a signature vertical gradient + * (RhoRed to SilverGray to Black) defining the app's visual identity. + * • Multi-ViewModel Architecture: Coordinates state between * HeaderViewModel (navigation/profile) and HomeViewModel (content). - * • Modular UI: Composed of reusable building blocks: PageHeader, + * • Modular UI: Composed of reusable building blocks: PageHeader, * ServiceList, and PageFooter. - * • Responsive Layout: Uses weighted components to ensure the + * • Responsive Layout: Uses weighted components to ensure the * ServiceList occupies available vertical space effectively. * ============================================================================ */ + package com.rho.studio.ui.features.home import androidx.compose.foundation.background @@ -38,13 +40,13 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.lifecycle.viewmodel.compose.viewModel -import com.rho.studio.ui.features.common.HeaderViewModel -import com.rho.studio.ui.features.common.PageFooter -import com.rho.studio.ui.features.common.PageHeader +import com.rho.studio.ui.core.ui.common.HeaderViewModel +import com.rho.studio.ui.core.ui.common.PageFooter +import com.rho.studio.ui.core.ui.common.PageHeader import com.rho.studio.ui.features.home.components.ServiceList -import com.rho.studio.ui.ui.theme.Black -import com.rho.studio.ui.ui.theme.RhoRed -import com.rho.studio.ui.ui.theme.SilverGray +import com.rho.studio.ui.core.ui.theme.Black +import com.rho.studio.ui.core.ui.theme.RhoRed +import com.rho.studio.ui.core.ui.theme.SilverGray @Composable fun HomeScreen( @@ -57,11 +59,7 @@ fun HomeScreen( .fillMaxSize() .background( brush = Brush.verticalGradient( - colors = listOf( - RhoRed, - SilverGray, - Black - ) + colors = listOf(RhoRed, SilverGray, Black) ) ) ) { @@ -76,7 +74,7 @@ fun HomeScreen( ) PageFooter( - viewModel = homeViewModel, + onLogoutClick = { homeViewModel.logout() }, modifier = Modifier.fillMaxWidth() ) } diff --git a/app/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 similarity index 65% rename from app/src/main/java/com/rho/studio/ui/features/home/HomeViewModel.kt rename to features/home/src/main/java/com/rho/studio/ui/features/home/HomeViewModel.kt index 00e998f..7d3e6e0 100644 --- a/app/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,30 +10,34 @@ * File: HomeViewModel.kt * Author: Alexis Tercero * Email: alexis.tercero@rho.studio - * Date: 2026-07-20 + * Date: 2026-08-06 * ============================================================================================== - * Description: ViewModel for the Home feature, managing dashboard state and session data. + * Description: ViewModel for the Home feature, managing feature state, session data, + * and providing access to available service modules. * ============================================================================================== */ package com.rho.studio.ui.features.home -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import com.rho.studio.ui.R -import com.rho.studio.ui.core.base.BaseViewModel -import com.rho.studio.ui.core.manager.SessionManager -import com.rho.studio.ui.core.model.ServiceModule -import com.rho.studio.ui.core.model.User +import com.rho.studio.ui.core.ui.R +import com.rho.studio.ui.core.ui.base.BaseViewModel +import com.rho.studio.ui.core.data.manager.SessionManager +import com.rho.studio.ui.features.home.model.ServiceModule +import com.rho.studio.ui.core.domain.model.User +import com.rho.studio.ui.core.domain.usecase.LogoutUseCase +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow class HomeViewModel : BaseViewModel() { private val sessionManager = SessionManager.getInstance() + private val logoutUseCase = LogoutUseCase(sessionManager) - private val _currentUser = MutableLiveData(sessionManager.getCurrentUserSync()) - val currentUser: LiveData = _currentUser + private val _currentUser = MutableStateFlow(sessionManager.getCurrentUserSync()) + val currentUser: StateFlow = _currentUser.asStateFlow() - // Parametrized services for the dashboard - private val _services = MutableLiveData>( + // Parametrized services for the Home experience + private val _services = MutableStateFlow>( listOf( ServiceModule("inv", R.string.module_inventory, R.color.rho_red), ServiceModule("sales", R.string.module_sales, R.color.rho_strong_gray), @@ -41,10 +45,10 @@ class HomeViewModel : BaseViewModel() { ServiceModule("rep", R.string.module_reports, R.color.rho_red) ) ) - val services: LiveData> = _services + val services: StateFlow> = _services.asStateFlow() - private val _navigateToService = MutableLiveData() - val navigateToService: LiveData = _navigateToService + private val _navigateToService = MutableStateFlow(null) + val navigateToService: StateFlow = _navigateToService.asStateFlow() fun onServiceClick(serviceId: String) { _navigateToService.value = serviceId @@ -54,13 +58,9 @@ class HomeViewModel : BaseViewModel() { _navigateToService.value = null } - /** - * Trigger user logout via SessionManager. - * The transition to the login screen is handled by the MainActivity observer. - */ fun logout() { launchWithLoading({ - sessionManager.logout().join() + logoutUseCase(Unit) }) } } diff --git a/app/src/main/java/com/rho/studio/ui/features/home/components/ServiceItem.kt b/features/home/src/main/java/com/rho/studio/ui/features/home/components/ServiceItem.kt similarity index 90% rename from app/src/main/java/com/rho/studio/ui/features/home/components/ServiceItem.kt rename to features/home/src/main/java/com/rho/studio/ui/features/home/components/ServiceItem.kt index 73bd2dd..39b9f9a 100644 --- a/app/src/main/java/com/rho/studio/ui/features/home/components/ServiceItem.kt +++ b/features/home/src/main/java/com/rho/studio/ui/features/home/components/ServiceItem.kt @@ -10,22 +10,24 @@ * File: ServiceItem.kt (composable UI) * Author: Alexis Tercero * Email: alexis.tercero@rho.studio - * Date: 2026-07-29 + * Date: 2026-08-06 * ============================================================================ - * Description: - * A modular UI component representing an individual service entry - * within the Home screen grid. It encapsulates the visual style + * Description: + * A modular UI component representing an individual service entry + * within the Home screen grid. It encapsulates the visual style * and interaction logic for a single ServiceModule. * * Key Features: - * • Adaptive Styling: Dynamically sets its background color based + * • Adaptive Styling: Dynamically sets its background color based * on the ServiceModule's resource definitions. - * • Geometric Design: Features a fixed aspect ratio and rounded + * • Geometric Design: Features a fixed 1:1 aspect ratio and rounded * corners to maintain UI consistency across the service grid. - * • Localized Content: Automatically resolves and displays title + * • Localized Content: Automatically resolves and displays title * strings from Android resource IDs. - * • Feedback: Built on Material 3 Button semantics to provide + * • Feedback: Built on Material 3 Button semantics to provide * standard touch feedback and accessibility support. + * • Scalable Grid Integration: Designed to be used within LazyVerticalGrid + * for responsive feature layouts. * ============================================================================ */ package com.rho.studio.ui.features.home.components @@ -42,7 +44,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import com.rho.studio.ui.core.model.ServiceModule +import com.rho.studio.ui.features.home.model.ServiceModule @Composable fun ServiceItem( diff --git a/app/src/main/java/com/rho/studio/ui/features/home/components/ServiceList.kt b/features/home/src/main/java/com/rho/studio/ui/features/home/components/ServiceList.kt similarity index 89% rename from app/src/main/java/com/rho/studio/ui/features/home/components/ServiceList.kt rename to features/home/src/main/java/com/rho/studio/ui/features/home/components/ServiceList.kt index 267bb3a..6c55397 100644 --- a/app/src/main/java/com/rho/studio/ui/features/home/components/ServiceList.kt +++ b/features/home/src/main/java/com/rho/studio/ui/features/home/components/ServiceList.kt @@ -10,20 +10,20 @@ * File: ServiceList.kt (composable UI) * Author: Alexis Tercero * Email: alexis.tercero@rho.studio - * Date: 2026-07-29 + * Date: 2026-08-06 * ============================================================================ - * Description: - * A grid-based component that displays the collection of available + * Description: + * A grid-based component that displays the collection of available * services. It acts as the primary content container for the HomeScreen. * * Key Features: - * • Adaptive Grid: Utilizes `LazyVerticalGrid` with a fixed column + * • Adaptive Grid: Utilizes `LazyVerticalGrid` with a fixed column * count to present service items in a clean, organized layout. - * • State Observation: Reactively observes the services list from - * the `HomeViewModel` using `observeAsState`. - * • Event Delegation: Forwards user interactions (clicks) back to + * • Flow Integration: Reactively observes the services stream from + * the `HomeViewModel` using `collectAsState`. + * • Event Delegation: Forwards user interactions (clicks) back to * the ViewModel for centralized business logic handling. - * • Performance: Efficiently renders large lists by utilizing + * • Performance: Efficiently renders large lists by utilizing * lazy-loading mechanics. * ============================================================================ */ @@ -35,8 +35,8 @@ import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.rho.studio.ui.features.home.HomeViewModel @@ -46,7 +46,7 @@ fun ServiceList( viewModel: HomeViewModel, modifier: Modifier = Modifier ) { - val services by viewModel.services.observeAsState(emptyList()) + val services by viewModel.services.collectAsState() LazyVerticalGrid( columns = GridCells.Fixed(2), diff --git a/features/home/src/main/java/com/rho/studio/ui/features/home/model/ServiceModule.kt b/features/home/src/main/java/com/rho/studio/ui/features/home/model/ServiceModule.kt new file mode 100644 index 0000000..5ccea25 --- /dev/null +++ b/features/home/src/main/java/com/rho/studio/ui/features/home/model/ServiceModule.kt @@ -0,0 +1,31 @@ +/** + * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗ + * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗ + * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝ + * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ + * + * ============================================================================ + * File: ServiceModule.kt (composable UI) + * Author: Alexis Tercero + * Email: alexis.tercero@rho.studio + * Date: 2026-08-06 + * ============================================================================ + * Description: + * Represents a service or module available on the home dashboard. + * ============================================================================ + */ +package com.rho.studio.ui.features.home.model + +import androidx.annotation.ColorRes +import androidx.annotation.StringRes + +/** + * Represents a service or module available on the home dashboard. + */ +data class ServiceModule( + val id: String, + @StringRes val titleRes: Int, + @ColorRes val backgroundColor: Int +) diff --git a/gradle.properties b/gradle.properties index 410c934..ae9a532 100644 --- a/gradle.properties +++ b/gradle.properties @@ -26,6 +26,8 @@ android.defaults.buildfeatures.resvalues=false android.sdk.defaultTargetSdkToCompileSdkIfUnset=true android.usesSdkInManifest.disallowed=true android.uniquePackageNames=false -android.dependency.useConstraints=true +android.dependency.useConstraints=false android.r8.strictFullModeForKeepRules=false android.r8.optimizedResourceShrinking=true +android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false +android.sync.suppressAgpWarnings=LIBRARY_CONSTRAINTS_SHOULD_BE_DISABLED,UNSUPPORTED_PROJECT_OPTION_USE diff --git a/gradle/gradle-daemon-jvm.properties b/gradle/gradle-daemon-jvm.properties new file mode 100644 index 0000000..6c1139e --- /dev/null +++ b/gradle/gradle-daemon-jvm.properties @@ -0,0 +1,12 @@ +#This file is generated by updateDaemonJvm +toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect +toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect +toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect +toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect +toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/73bcfb608d1fde9fb62e462f834a3299/redirect +toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/846ee0d876d26a26f37aa1ce8de73224/redirect +toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect +toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect +toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/9482ddec596298c84656d31d16652665/redirect +toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/39701d92e1756bb2f141eb67cd4c660e/redirect +toolchainVersion=21 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e17f771..124494d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "9.2.1" +agp = "9.3.1" compiler = "3.1.4" fragmentKtx = "1.8.9" gson = "2.14.0" @@ -13,7 +13,6 @@ lifecycleRuntimeKtx = "2.11.0" lifecycleViewmodelCompose = "2.11.0" activityCompose = "1.13.0" composeBom = "2026.06.01" -composeRuntimeLivedata = "1.8.0-alpha08" material = "1.14.0" navigation = "2.9.8" @@ -29,7 +28,6 @@ androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecyc androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } -androidx-compose-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata", version.ref = "composeRuntimeLivedata" } androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" } androidx-ui = { group = "androidx.compose.ui", name = "ui" } androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } @@ -45,6 +43,8 @@ material = { group = "com.google.android.material", name = "material", version.r [plugins] android-application = { id = "com.android.application", version.ref = "agp" } +android-library = { id = "com.android.library", version.ref = "agp" } +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" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 914b2d2..a4d1676 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -24,4 +24,9 @@ dependencyResolutionManagement { rootProject.name = "UI" include(":app") +include(":core:domain") +include(":core:data") +include(":core:ui") +include(":features:auth") +include(":features:home") \ No newline at end of file