diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
index 1af7126..db7e80f 100644
--- a/.github/workflows/android.yml
+++ b/.github/workflows/android.yml
@@ -3,9 +3,9 @@ name: Android CI/CD Rho.Studio®
on:
workflow_dispatch:
push:
- branches: [ "pre-release" ]
+ branches: [ "19-compose-migration-review" ]
pull_request:
- branches: [ "main" ]
+ branches: [ "dev" , "pre-release" , "main" ]
jobs:
build:
diff --git a/README.md b/README.md
index 29087e0..39adf0e 100644
--- a/README.md
+++ b/README.md
@@ -1,18 +1,104 @@
-# Rho Studio UI App
-#### An Android View System app.
+# 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.
-
+
-## Architecture
+---
-- MVVM Architecture for managing the app and code.
-- Single-activity Android architecture.
-- Android View System for UI.
-- Session manager.
+## 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).
-### Current navigation graph
+---
-
+## 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.1 UI & Feature Layers (View)
+The UI is composed of stateless screens and modular components that observe state from their respective ViewModels.
-[**Rho.Studio®**](https://rho.studio/)
+- **`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).
+
+### 2.2 Business Logic & State Layer (ViewModel)
+ViewModels act as the bridge between features and the data layer, handling user intent and reactive state.
+
+- **`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`.
+
+### 2.3 Core Data & Infrastructure Layer
+Provides the essential services and "Single Source of Truth" for the entire application.
+
+- **`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.
+
+---
+
+## 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.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.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.
+
+---
+
+## 4. File Registry & Responsibilities
+
+| 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. |
+
+---
+
+## 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.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)
+
+### 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)
+
+### 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)
+
+---
+
+## 6. 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)
+
+---
+**[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 b26cc37..cc9120e 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -2,7 +2,6 @@ plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.parcelize)
- alias(libs.plugins.legacy.kapt)
}
android {
@@ -35,8 +34,6 @@ android {
buildFeatures {
compose = true
- dataBinding = true
- viewBinding = true // Optional but recommended
}
sourceSets {
@@ -66,15 +63,16 @@ dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.activity.compose)
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.fragment.ktx)
- implementation(libs.androidx.navigation.fragment.ktx)
- implementation(libs.androidx.navigation.ui.ktx)
+ implementation(libs.androidx.compose.material.icons.extended)
+ implementation(libs.androidx.compose.runtime.livedata)
+ implementation(libs.androidx.navigation.compose)
implementation(libs.gson)
implementation(libs.material)
testImplementation(libs.junit)
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 6faf781..1bd3030 100644
--- a/app/src/main/java/com/rho/studio/ui/MainActivity.kt
+++ b/app/src/main/java/com/rho/studio/ui/MainActivity.kt
@@ -10,189 +10,136 @@
* File: MainActivity.kt
* Author: Alexis Tercero
* Email: alexis.tercero@rho.studio
- * Date: 2026-07-20
+ * Date: 2026-07-29
* ==========================================================================
* Description:
- * This activity follows the "Single Activity" architecture pattern, acting as the
- * main orchestrator for fragment navigation and global state management.
+ * The primary entry point for the RHO Studio application, migrated to
+ * pure Jetpack Compose.
+ * Screen Assembly: Built LoginScreen and HomeScreen to unify the components.
+ * 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.
* ==========================================================================
*/
package com.rho.studio.ui
import android.os.Bundle
-import android.util.Log
-import android.view.View
import android.widget.Toast
-import androidx.appcompat.app.AppCompatActivity
-import androidx.databinding.DataBindingUtil
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.Box
+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.getValue
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
import androidx.lifecycle.ViewModelProvider
-import androidx.navigation.NavController
-import androidx.navigation.fragment.NavHostFragment
+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.databinding.ActivityMainBinding
-import com.rho.studio.ui.features.auth.LoginFragment
+import com.rho.studio.ui.features.auth.LoginScreen
import com.rho.studio.ui.features.auth.LoginViewModel
-import com.rho.studio.ui.features.home.HomeFragment
+import com.rho.studio.ui.features.home.HomeScreen
+import com.rho.studio.ui.features.home.HomeViewModel
+import com.rho.studio.ui.ui.theme.UITheme
-/**
- * The primary entry point and root container for the RHO Studio application.
- *
- * This activity follows the "Single Activity" architecture pattern, acting as the
- * main orchestrator for fragment navigation and global state management.
- *
- * ### Key Responsibilities:
- * 1. **Initialization:** Bootstraps the [SessionManager] and core ViewModels.
- * 2. **Authentication Routing:** Observes [SessionManager.isAuthenticated] to
- * automatically toggle between the login flow and the home dashboard.
- * 3. **Global Error Handling:** Implements a top-level [Thread.UncaughtExceptionHandler]
- * to log and display fatal crashes during development.
- * 4. **Resource Management:** Ensures the [SessionManager] is cleaned up during
- * the activity destruction to prevent memory leaks.
- *
- * ### UI Components:
- * - Uses [ActivityMainBinding] for layout management.
- * - Hosts fragments within the `main_container` (ID: R.id.main_container).
- * - Manages a global progress indicator synchronized with [SessionManager.isLoading].
- */
-class MainActivity : AppCompatActivity() {
+class MainActivity : ComponentActivity() {
- private lateinit var binding: ActivityMainBinding
private lateinit var sessionManager: SessionManager
private lateinit var loginViewModel: LoginViewModel
- private lateinit var navController: NavController
- private var isNavGraphReady = false
+ private lateinit var homeViewModel: HomeViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- try {
- // Set a default error handler
- Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
- Log.e("MainActivityCrash", "Uncaught exception", throwable)
- // Show error in a Toast (might not work if UI thread is dead)
- runOnUiThread {
- Toast.makeText(
- this,
- "Crash: ${throwable.message}",
- Toast.LENGTH_LONG
- ).show()
- }
- }
-
- initializeBinding()
- initializeManagers()
- setupObservers()
+ initializeManagers()
- // Removed showInitialScreen() as handleAuthStateChange
- // will be triggered by the SessionManager observer automatically.
- } catch (e: Exception) {
- Log.e("MainActivity", "Initialization failed", e)
- Toast.makeText(this, "Error: ${e.message}", Toast.LENGTH_LONG).show()
- finish()
+ setContent {
+ UITheme {
+ MainContent()
+ }
}
}
- override fun onSaveInstanceState(outState: Bundle) {
- super.onSaveInstanceState(outState)
- outState.putBoolean("is_initialized", true)
- }
-
- private fun initializeBinding() {
- binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
- binding.lifecycleOwner = this
-
- val navHostFragment = supportFragmentManager
- .findFragmentById(R.id.main_container) as NavHostFragment
- navController = navHostFragment.navController
- }
-
private fun initializeManagers() {
SessionManager.init(applicationContext)
sessionManager = SessionManager.getInstance()
loginViewModel = ViewModelProvider(this)[LoginViewModel::class.java]
- binding.sessionManager = sessionManager
- }
-
- private fun setupObservers() {
- // Wait for session check before deciding initial route
- sessionManager.isSessionChecked.observe(this) { isChecked ->
- if (isChecked) {
- handleAuthStateChange(sessionManager.isAuthenticatedSync())
- } else {
- // Show loading while checking
- binding.progressBar.visibility = View.VISIBLE
- }
- }
-
- sessionManager.isAuthenticated.observe(this) { isAuthenticated ->
- // Only handle subsequent changes if graph is already ready
- if (isNavGraphReady) {
- handleAuthStateChange(isAuthenticated)
- }
- }
-
- sessionManager.isLoading.observe(this) { isLoading ->
- // Combine with isSessionChecked logic
- if (sessionManager.isSessionChecked.value == true) {
- binding.progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE
- }
- }
-
+ homeViewModel = ViewModelProvider(this)[HomeViewModel::class.java]
+
sessionManager.error.observe(this) { error ->
error?.let {
- android.widget.Toast.makeText(this, it, Toast.LENGTH_LONG).show()
+ Toast.makeText(this, it, Toast.LENGTH_LONG).show()
sessionManager.clearError()
}
}
}
- private fun showLoginScreen() {
- val currentDest = navController.currentDestination?.id
- if (currentDest != null && currentDest != R.id.loginFragment) {
- navController.navigate(R.id.action_homeFragment_to_loginFragment)
- }
- }
+ @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)
- private fun showHomeScreen() {
- val currentDest = navController.currentDestination?.id
- if (currentDest != null && currentDest != R.id.homeFragment) {
- navController.navigate(R.id.action_loginFragment_to_homeFragment)
+ if (!isSessionChecked) {
+ LoadingScreen()
+ return
}
- }
- private fun handleAuthStateChange(isAuthenticated: Boolean) {
- Log.d("MainActivity", "Auth state change: isAuthenticated = $isAuthenticated")
-
- if (!isNavGraphReady) {
- setupNavGraph(isAuthenticated)
- isNavGraphReady = true
- // Hide initial loading
- binding.progressBar.visibility = if (sessionManager.isLoading.value == true)
- View.VISIBLE else View.GONE
- } else {
+ // Handle navigation based on auth state
+ LaunchedEffect(isAuthenticated) {
if (isAuthenticated) {
- showHomeScreen()
- loginViewModel.resetForm()
+ navController.navigate("home") {
+ popUpTo("login") { inclusive = true }
+ }
} else {
- showLoginScreen()
+ loginViewModel.resetForm()
+ navController.navigate("login") {
+ popUpTo("home") { inclusive = true }
+ }
}
}
- }
- /**
- * Set up the Navigation Graph programmatically to avoid the "Start Destination" flicker.
- */
- private fun setupNavGraph(isAuthenticated: Boolean) {
- val navInflater = navController.navInflater
- val graph = navInflater.inflate(R.navigation.nav_graph)
+ Box(modifier = Modifier.fillMaxSize()) {
+ NavHost(
+ navController = navController,
+ startDestination = if (isAuthenticated) "home" else "login"
+ ) {
+ composable("login") {
+ LoginScreen(viewModel = loginViewModel)
+ }
+ composable("home") {
+ HomeScreen(homeViewModel = homeViewModel)
+ }
+ }
- // Choose start destination based on authentication state
- graph.setStartDestination(if (isAuthenticated) R.id.homeFragment else R.id.loginFragment)
-
- navController.graph = graph
+ if (isLoading) {
+ LoadingOverlay()
+ }
+ }
}
- fun getLoginViewModel(): LoginViewModel = loginViewModel
+ @Composable
+ private fun LoadingScreen() {
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ CircularProgressIndicator()
+ }
+ }
+
+ @Composable
+ private fun LoadingOverlay() {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ CircularProgressIndicator()
+ }
+ }
override fun onDestroy() {
super.onDestroy()
diff --git a/app/src/main/java/com/rho/studio/ui/core/base/BaseFragment.kt b/app/src/main/java/com/rho/studio/ui/core/base/BaseFragment.kt
deleted file mode 100644
index 18aaa72..0000000
--- a/app/src/main/java/com/rho/studio/ui/core/base/BaseFragment.kt
+++ /dev/null
@@ -1,220 +0,0 @@
-/**
- * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
- * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
- * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
- * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
- * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
- * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
- *
- * ==============================================================================================
- * File: BaseFragment.kt
- * Author: Alexis Tercero
- * Email: alexis.tercero@rho.studio
- * Date: 2026-07-15
- * ==============================================================================================
- * Description:
- * A generic base class for [Fragment]s that utilize DataBinding and BaseViewModel.
- * ==============================================================================================
- */
-package com.rho.studio.ui.core.base
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.Toast
-import androidx.annotation.LayoutRes
-import androidx.databinding.DataBindingUtil
-import androidx.databinding.ViewDataBinding
-import androidx.fragment.app.Fragment
-import androidx.lifecycle.ViewModel
-import com.rho.studio.ui.core.manager.SessionManager
-
-/**
- * BaseFragment - Base class for all feature fragments
- *
- * A generic base class for [Fragment]s that utilize DataBinding and [BaseViewModel].
- *
- * This base class standardizes the fragment lifecycle, enforces memory leak prevention
- * for view bindings, and provides common hooks for initialization and observation.
- *
- * ### Key Features:
- * 1. **Automated DataBinding:** Inflates the layout and attaches the ViewModel automatically.
- * 2. **Lifecycle Safety:** Manages the backing property for [_binding] to prevent
- * memory leaks by nulling it out in [onDestroyView].
- * 3. **Session Integration:** Provides lazy access to the global [SessionManager].
- * 4. **Standardized Workflow:** Defines a clear execution order: Binding -> [initializeViews] -> [setupObservers].
- *
- * ### How to implement:
- * ```kotlin
- * class LoginFragment : BaseFragment() {
- * override val viewModel: LoginViewModel by viewModels()
- * override val layoutId: Int = R.layout.fragment_login
- * override val bindingVariable: Int = BR.viewModel
- *
- * override fun initializeViews() {
- * binding.loginButton.setOnClickListener { ... }
- * }
- * }
- * ```
- *
- * @param T The specific [ViewDataBinding] class generated for the fragment's layout.
- * @param VM The [ViewModel] class associated with this fragment.
- */
-abstract class BaseFragment : Fragment() {
-
- // ==================== ABSTRACT PROPERTIES ====================
-
- /**It declares a read-only property named viewModel of type VM
- * (the specific ViewModel type provided when the subclass is created).*/
- protected abstract val viewModel: VM
-
- @get:LayoutRes
- protected abstract val layoutId: Int
-
- protected abstract val bindingVariable: Int
-
- // ==================== BINDING WITH MEMORY LEAK PROTECTION ====================
-
- private var _binding: T? = null
-
- /**
- * Protected binding property - safe access only between onCreateView and onDestroyView
- * Throws IllegalStateException if accessed outside this window
- */
- protected val binding: T
- get() = _binding ?: throw IllegalStateException(
- "Cannot access binding after onDestroyView or before onCreateView"
- )
-
- // ==================== OPTIONAL DEPENDENCIES ====================
-
- /**
- * SessionManager - lazy initialized, only created if accessed
- * Made open so fragments can override if needed
- */
- protected open val sessionManager: SessionManager by lazy {
- SessionManager.getInstance()
- }
-
- // ==================== LIFECYCLE METHODS ====================
-
- final override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View {
- //backing property
- _binding = DataBindingUtil.inflate(inflater, layoutId, container, false)
-
- /** snippet is used in
- * Android Data Binding to connect your layout views to a data source
- * (usually a ViewModel) and ensure the UI reflects changes immediately.
- * */
- with(binding) {
- /**layout uses LiveData, the binding needs a lifecycle owner to observe that data.
- * Without this line, LiveData changes in your ViewModel
- * will not automatically update the UI*/
- lifecycleOwner = viewLifecycleOwner
- setVariable(bindingVariable, viewModel)
- executePendingBindings() // Immediate UI update
- }
-
- return binding.root
- }
-
- /**
- * Set up views and observers in this method
- */
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- setupCommonObservers()
- initializeViews()
- setupObservers()
- }
-
- /**
- * Critical for memory leak prevention
- */
- final override fun onDestroyView() {
- cleanupBinding()
- super.onDestroyView()
- _binding = null
- }
-
- /**
- * Sets up automatic observation of common ViewModel LiveData.
- * This eliminates boilerplate in child fragments.
- */
- private fun setupCommonObservers() {
- // Loading state - override onLoadingStateChanged for custom UI
- viewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
- onLoadingStateChanged(isLoading)
- }
-
- // Error messages - automatically shown and cleared
- viewModel.error.observe(viewLifecycleOwner) { error ->
- error?.let {
- onError(it)
- viewModel.clearError()
- }
- }
-
- // Toast messages - automatically shown and cleared
- viewModel.toastMessage.observe(viewLifecycleOwner) { message ->
- message?.let {
- onToastMessage(it)
- viewModel.clearToastMessage()
- }
- }
- }
-
- // ==================== EXTENSION POINTS FOR CHILD FRAGMENTS ====================
-
- /**
- * Called after binding is set up - use for view initialization
- * Examples: setting up RecyclerView, adapters, click listeners
- */
- protected open fun initializeViews() {}
-
- /**
- * Called after initializeViews - use for LiveData observers
- * Separated from initializeViews for better organization
- */
- protected open fun setupObservers() {}
-
- /**
- * Optional cleanup method for fragments that need to release resources
- * Called before binding is nulled
- */
- protected open fun cleanupBinding() {
- // Override in child fragments if needed
- }
-
- // ==================== UTILITY METHODS ====================
-
- /** Called when loading state changes - override for custom loading UI */
- protected open fun onLoadingStateChanged(isLoading: Boolean) {}
-
- /** Called when an error occurs - override for custom error handling */
- protected open fun onError(message: String) {
- Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show()
- }
-
- /** Called for toast messages - override for custom toast behavior */
- protected open fun onToastMessage(message: String) {
- Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
- }
-
- /**
- * Check if binding is available (between onCreateView and onDestroyView)
- */
- protected fun isBindingAvailable(): Boolean = _binding != null
-
- /**
- * Safely execute code that requires binding
- */
- protected fun withBinding(block: (T) -> Unit) {
- _binding?.let(block)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/rho/studio/ui/features/auth/LoginFragment.kt b/app/src/main/java/com/rho/studio/ui/features/auth/LoginFragment.kt
deleted file mode 100644
index c0b37b6..0000000
--- a/app/src/main/java/com/rho/studio/ui/features/auth/LoginFragment.kt
+++ /dev/null
@@ -1,91 +0,0 @@
-/**
- * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
- * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
- * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
- * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
- * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
- * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
- *
- * ==============================================================================================
- * File: LoginFragment.kt
- * Author: Alexis Tercero
- * Email: alexis.tercero@rho.studio
- * Date: 2026-07-16
- * ==============================================================================================
- * LoginFragment serves as the primary orchestration layer for the authentication feature.
- * It acts as a parent container that manages the lifecycle, state observation,
- * and composition of specialized login components. It inherits from BaseFragment
- * to leverage standardized ViewBinding and ViewModel integration.
- * ==============================================================================================
- */
-package com.rho.studio.ui.features.auth
-
-import android.view.View
-import androidx.fragment.app.viewModels
-import com.rho.studio.ui.BR
-import com.rho.studio.ui.R
-import com.rho.studio.ui.core.base.BaseFragment
-import com.rho.studio.ui.databinding.FragmentLoginBinding
-import com.rho.studio.ui.features.auth.components.LoginButtonFragment
-import com.rho.studio.ui.features.auth.components.LoginEmailFragment
-import com.rho.studio.ui.features.auth.components.LoginPasswordFragment
-
-
-/**
- * LoginFragment - Main container for authentication
- *
- * Feature: auth
- *
- * Responsibilities:
- * • Reusability: Individual login components can be reused in other flows
- * (e.g., Registration).
- * • Separation of Concerns: LoginFragment manages the "How" (layout/navigation),
- * while child fragments handle the "What" (specific inputs).
- *
- * 1. Fragment Composition
- * The fragment initializes the UI by embedding three core sub-components
- * into designated containers within fragment_login.xml:
- * •LoginEmailFragment: Handles email input and validation.
- * •LoginPasswordFragment: Handles password input and visibility.
- * •LoginButtonFragment: Handles the submission trigger.
- * 2. State Observation
- * It observes the LoginViewModel to react to the following states:
- * •Toast Messages: Short-lived UI feedback (e.g., "Welcome back").
- * •Errors: Long-lived feedback for failed authentication attempts.
- * •Loading State: Toggles the visibility of a global ProgressBar
- * to block interaction during network requests.
- * 3. Lifecycle Management
- * •Initialization:
- * Uses childFragmentManager to transactionally inject components
- * once the fragment is attached.
- * •Cleanup:
- * Ensures that transient UI states (like error messages or toasts)
- * are cleared from the ViewModel when the view is destroyed
- * to prevent stale data on return.
- */
-class LoginFragment : BaseFragment() {
-
- override val viewModel: LoginViewModel by viewModels()
- override val layoutId: Int = R.layout.fragment_login
- override val bindingVariable: Int = BR.viewModel
-
- override fun initializeViews() {
- if (isAdded) {
- setupChildFragments()
- }
- }
-
- override fun cleanupBinding() {
- // Enforce clean slate policy defined in feature docs
- viewModel.clearToastMessage()
- viewModel.clearError()
- }
- private fun setupChildFragments() {
- childFragmentManager.beginTransaction().apply {
- replace(R.id.email_container, LoginEmailFragment())
- replace(R.id.password_container, LoginPasswordFragment())
- replace(R.id.button_container, LoginButtonFragment())
- commitAllowingStateLoss()
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/rho/studio/ui/features/auth/LoginScreen.kt b/app/src/main/java/com/rho/studio/ui/features/auth/LoginScreen.kt
new file mode 100644
index 0000000..b5a43bf
--- /dev/null
+++ b/app/src/main/java/com/rho/studio/ui/features/auth/LoginScreen.kt
@@ -0,0 +1,101 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ============================================================================
+ * 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.
+ * ============================================================================
+ */
+package com.rho.studio.ui.features.auth
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+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
+import androidx.compose.ui.graphics.Color
+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.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
+
+@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
+ )
+ )
+ )
+ .padding(24.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = stringResource(R.string.app_name),
+ fontSize = 32.sp,
+ fontWeight = FontWeight.Bold,
+ color = Color.Black
+ )
+ Spacer(modifier = Modifier.height(48.dp))
+ LoginEmailField(viewModel = viewModel)
+ Spacer(modifier = Modifier.height(16.dp))
+ LoginPasswordField(viewModel = viewModel)
+ Spacer(modifier = Modifier.height(24.dp))
+ LoginButton(viewModel = viewModel)
+ }
+ }
+}
diff --git a/app/src/main/java/com/rho/studio/ui/features/auth/LoginViewModel.kt b/app/src/main/java/com/rho/studio/ui/features/auth/LoginViewModel.kt
index 52e3434..cfa794b 100644
--- a/app/src/main/java/com/rho/studio/ui/features/auth/LoginViewModel.kt
+++ b/app/src/main/java/com/rho/studio/ui/features/auth/LoginViewModel.kt
@@ -10,13 +10,14 @@
* File: LoginViewModel.kt
* Author: Alexis Tercero
* Email: alexis.tercero@rho.studio
- * Date: 2026-07-16
+ * Date: 2026-07-29
* ============================================================================
* Description:
* The LoginViewModel manages the state and business logic for the
- * Authentication screen, leveraging the Rho Studio BaseViewModel architecture.
- * It handles user input validation, manages asynchronous login requests
- * via SessionManager, and exposes reactive UI states using LiveData.
+ * 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.
*
* •Extends: com.rho.studio.ui.core.base.BaseViewModel
* •Dependencies:
@@ -24,12 +25,14 @@
* •Credentials: A data model encapsulating email and password logic.
*
* Core Logic Flows
- * Real-time Validation
+ * State-Driven Input
+ * •email / password: Uses Compose `mutableStateOf` to provide
+ * immediate, observable reactivity for the UI layer.
+ * Real-time & Debounced Validation
* •onEmailChanged() / onPasswordChanged():
- * Triggered on every keystroke.
- * •Updates the credentials model and
- * immediately evaluates validation rules
- * (blank checks, email regex, password length).
+ * Triggered on every keystroke, updating the state immediately.
+ * •300ms Debounce: Logic moved from Fragments to the ViewModel,
+ * ensuring validation is only performed after the user pauses typing.
* Authentication Process
* •Trigger: onLoginClick() performs final validation and guards
* against concurrent attempts using the base loading state.
@@ -44,52 +47,67 @@
* Relies on BaseViewModel's automated job tracking and cleanup
* to prevent memory leaks without manual cancellation logic.
* •State Reset:
- * resetForm() provides a clean slate for the UI.
+ * resetForm() provides a clean, secure slate for the UI by
+ * clearing Compose states and the underlying model.
* ============================================================================
*/
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 kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
class LoginViewModel : BaseViewModel() {
-
// ==================== DEPENDENCIES ====================
-
private val sessionManager = SessionManager.getInstance()
private var loginJob: Job? = null
-
+ private var emailDebounceJob: Job? = null
+ private var passwordDebounceJob: Job? = null
// ==================== FORM STATE ====================
-
+ var email by mutableStateOf("")
+ private set
+ var password by mutableStateOf("")
+ 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
-
// ==================== FORM VALIDATION ====================
-
fun onEmailChanged(email: String) {
+ this.email = email
credentials.email = email
- validateEmail()
- validateForm()
+
+ emailDebounceJob?.cancel()
+ emailDebounceJob = viewModelScope.launch {
+ delay(300)
+ validateEmail()
+ validateForm()
+ }
}
fun onPasswordChanged(password: String) {
+ this.password = password
credentials.password = password
- validatePassword()
- validateForm()
+
+ passwordDebounceJob?.cancel()
+ passwordDebounceJob = viewModelScope.launch {
+ delay(300)
+ validatePassword()
+ validateForm()
+ }
}
private fun validateEmail() {
@@ -113,7 +131,6 @@ class LoginViewModel : BaseViewModel() {
}
// ==================== ACTIONS ====================
-
fun onLoginClick() {
// Guard against multiple concurrent login attempts
if (isLoading.value == true) return
@@ -152,8 +169,9 @@ class LoginViewModel : BaseViewModel() {
}
// ==================== UTILITY METHODS ====================
-
fun resetForm() {
+ email = ""
+ password = ""
credentials.clear()
_emailError.value = null
_passwordError.value = null
diff --git a/app/src/main/java/com/rho/studio/ui/features/auth/components/LoginButton.kt b/app/src/main/java/com/rho/studio/ui/features/auth/components/LoginButton.kt
new file mode 100644
index 0000000..c715b8a
--- /dev/null
+++ b/app/src/main/java/com/rho/studio/ui/features/auth/components/LoginButton.kt
@@ -0,0 +1,57 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ============================================================================
+ * File: LoginButton.kt (composable UI)
+ * 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.
+ * ============================================================================
+ */
+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.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.features.auth.LoginViewModel
+import com.rho.studio.ui.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)
+
+ Button(
+ onClick = { viewModel.onLoginClick() },
+ modifier = modifier
+ .fillMaxWidth()
+ .height(56.dp),
+ enabled = isFormValid && !isLoading,
+ colors = ButtonDefaults.buttonColors(containerColor = RhoRed)
+ ) {
+ 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/LoginButtonFragment.kt b/app/src/main/java/com/rho/studio/ui/features/auth/components/LoginButtonFragment.kt
deleted file mode 100644
index 78d2c95..0000000
--- a/app/src/main/java/com/rho/studio/ui/features/auth/components/LoginButtonFragment.kt
+++ /dev/null
@@ -1,111 +0,0 @@
-/**
- * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
- * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
- * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
- * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
- * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
- * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
- *
- * ==============================================================================================
- * File: LoginButtonFragment.kt
- * Author: Alexis Tercero
- * Email: alexis.tercero@rho.studio
- * Date: 2026-07-16
- * ==============================================================================================
- * Description: Manages the primary action button and loading
- * state for the login flow.
- * ==============================================================================================
- */
-package com.rho.studio.ui.features.auth.components
-
-import android.os.Bundle
-import android.view.View
-import android.widget.Toast
-import androidx.databinding.BindingAdapter
-import androidx.fragment.app.viewModels
-import androidx.lifecycle.lifecycleScope
-import com.rho.studio.ui.BR
-import com.rho.studio.ui.R
-import com.rho.studio.ui.core.base.BaseFragment
-import com.rho.studio.ui.databinding.FragmentLoginButtonBinding
-import com.rho.studio.ui.features.auth.LoginViewModel
-import kotlinx.coroutines.launch
-
-/**
- * LoginEmailFragment - Reusable email input component
- *
- * Feature: auth
- * Purpose: Manages the primary action button and loading
- * state for the login flow.
- *
- * This fragment acts as a reactive component within the Auth module. It synchronizes
- * the button's enabled state with form validation and displays progress indicators
- * during asynchronous login operations.
- *
- * ### Key Behaviors:
- * 1. **Shared State:** Scoped to the parent fragment via [requireParentFragment] to
- * interact with the shared [LoginViewModel].
- * 2. **Reactive UI:** Automatically enables/disables the login button based on
- * [LoginViewModel.isFormValid] and [LoginViewModel.isLoading].
- * 3. **Visual Feedback:** Manages the visibility of a progress bar during the
- * authentication network simulation.
- * 4. **Binding Adapters:** Provides a static [showToast] adapter to allow the XML
- * layout to reactively trigger system toasts based on ViewModel messages.
- *
- * ### Usage in XML:
- * ```xml
- *
- * ```
- */
-class LoginButtonFragment : BaseFragment() {
-
- override val viewModel: LoginViewModel by viewModels(ownerProducer = { requireParentFragment() })
- override val layoutId: Int = R.layout.fragment_login_button
- override val bindingVariable: Int = BR.viewModel
-
- companion object {
- @JvmStatic
- @BindingAdapter("toastMessage")
- fun showToast(view: View, message: String?) {
- message?.takeIf { it.isNotEmpty() }?.let {
- Toast.makeText(view.context, it, Toast.LENGTH_SHORT).show()
- }
- }
- }
-
- override fun initializeViews() {
- setupButton()
- }
-
- override fun setupObservers() {
- viewModel.isFormValid.observe(viewLifecycleOwner) { isValid ->
- withBinding { binding ->
- binding.loginButton.isEnabled = isValid
- }
- }
-
- viewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
- withBinding { binding ->
- binding.loginButton.isEnabled = !isLoading && (viewModel.isFormValid.value ?: false)
- }
- }
- }
-
- private fun setupButton() {
- withBinding { binding ->
- binding.loginButton.setOnClickListener {
- if (!binding.loginButton.isEnabled) return@setOnClickListener
-
- lifecycleScope.launch {
- viewModel.onLoginClick()
- }
- }
- }
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- view.tag = viewModel
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/rho/studio/ui/features/auth/components/LoginEmailField.kt b/app/src/main/java/com/rho/studio/ui/features/auth/components/LoginEmailField.kt
new file mode 100644
index 0000000..f7058f9
--- /dev/null
+++ b/app/src/main/java/com/rho/studio/ui/features/auth/components/LoginEmailField.kt
@@ -0,0 +1,82 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ============================================================================
+ * File: LoginEmailField.kt (composable UI)
+ * 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.
+ * ============================================================================
+ */
+package com.rho.studio.ui.features.auth.components
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Email
+import androidx.compose.material3.Icon
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.OutlinedTextFieldDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+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.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
+
+@Composable
+fun LoginEmailField(
+ viewModel: LoginViewModel,
+ modifier: Modifier = Modifier
+) {
+ val emailError by viewModel.emailError.observeAsState()
+
+ OutlinedTextField(
+ value = viewModel.email,
+ onValueChange = {
+ viewModel.onEmailChanged(it)
+ },
+ label = { Text(stringResource(R.string.email_hint)) },
+ modifier = modifier.fillMaxWidth(),
+ isError = emailError != null,
+ colors = OutlinedTextFieldDefaults.colors(
+ focusedBorderColor = RhoStrongGray,
+ unfocusedBorderColor = SilverGray,
+ errorBorderColor = ErrorRed,
+ focusedLabelColor = RhoStrongGray,
+ unfocusedLabelColor = TitleGray,
+ focusedLeadingIconColor = RhoStrongGray,
+ unfocusedLeadingIconColor = TitleGray
+ ),
+ supportingText = {
+ if (emailError != null) {
+ Text(text = emailError!!)
+ }
+ },
+ leadingIcon = {
+ Icon(imageVector = Icons.Default.Email, contentDescription = null)
+ },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email),
+ singleLine = true
+ )
+}
diff --git a/app/src/main/java/com/rho/studio/ui/features/auth/components/LoginEmailFragment.kt b/app/src/main/java/com/rho/studio/ui/features/auth/components/LoginEmailFragment.kt
deleted file mode 100644
index 6bbdf65..0000000
--- a/app/src/main/java/com/rho/studio/ui/features/auth/components/LoginEmailFragment.kt
+++ /dev/null
@@ -1,92 +0,0 @@
-/**
- * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
- * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
- * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
- * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
- * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
- * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
- *
- * ==============================================================================================
- * File: LoginEmailFragment.kt
- * Author: Alexis Tercero
- * Email: alexis.tercero@rho.studio
- * Date: 2026-02-23
- * ==============================================================================================
- * Description: Handles email input with two-way binding
- * ==============================================================================================
- */
-package com.rho.studio.ui.features.auth.components
-
-import android.text.Editable
-import android.text.TextWatcher
-import androidx.fragment.app.viewModels
-import androidx.lifecycle.lifecycleScope
-import com.rho.studio.ui.BR
-import com.rho.studio.ui.R
-import com.rho.studio.ui.core.base.BaseFragment
-import com.rho.studio.ui.databinding.FragmentLoginEmailBinding
-import com.rho.studio.ui.features.auth.LoginViewModel
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-/**
- * LoginEmailFragment - Reusable email input component
- *
- * Feature: auth
- * Purpose: Handles email input with two-way binding
- *
- * ### Key Behaviors:
- * 1. **Shared State:** Utilizes [requireParentFragment] as the ViewModel owner to
- * synchronize data with the main login flow.
- * 2. **Input Debouncing:** Implements a 300ms delay on text changes to prevent
- * excessive validation calls and UI flickering while the user is typing.
- * 3. **Error Feedback:** Observes the [LoginViewModel.emailError] to provide
- * real-time visual feedback via [com.google.android.material.textfield.TextInputLayout].
- *
- * ### UI Components:
- * - Uses [FragmentLoginEmailBinding] for direct access to input views.
- * - Managed within the `auth` feature module.
- */
-class LoginEmailFragment : BaseFragment() {
-
- override val viewModel: LoginViewModel by viewModels(ownerProducer = { requireParentFragment() })
- override val layoutId: Int = R.layout.fragment_login_email
- override val bindingVariable: Int = BR.viewModel
-
- private var debounceJob: Job? = null
-
- override fun initializeViews() {
- setupEmailInput()
- }
-
- override fun setupObservers() {
- viewModel.emailError.observe(viewLifecycleOwner) { error ->
- withBinding { binding ->
- binding.textInputLayout.error = error
- }
- }
- }
-
- private fun setupEmailInput() {
- withBinding { binding ->
- binding.emailInput.addTextChangedListener(object : TextWatcher {
- override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
-
- override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
- debounceJob?.cancel()
- debounceJob = lifecycleScope.launch {
- delay(300)
- viewModel.onEmailChanged(s?.toString() ?: "")
- }
- }
-
- override fun afterTextChanged(s: Editable?) {}
- })
- }
- }
-
- override fun cleanupBinding() {
- debounceJob?.cancel()
- debounceJob = null
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/rho/studio/ui/features/auth/components/LoginPasswordField.kt b/app/src/main/java/com/rho/studio/ui/features/auth/components/LoginPasswordField.kt
new file mode 100644
index 0000000..1248616
--- /dev/null
+++ b/app/src/main/java/com/rho/studio/ui/features/auth/components/LoginPasswordField.kt
@@ -0,0 +1,107 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ============================================================================
+ * File: LoginPasswordField.kt (composable UI)
+ * 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.
+ * ============================================================================
+ */
+package com.rho.studio.ui.features.auth.components
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Lock
+import androidx.compose.material.icons.filled.Visibility
+import androidx.compose.material.icons.filled.VisibilityOff
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.OutlinedTextFieldDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+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 androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.text.input.VisualTransformation
+import com.rho.studio.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
+
+@Composable
+fun LoginPasswordField(
+ viewModel: LoginViewModel,
+ modifier: Modifier = Modifier
+) {
+ val passwordError by viewModel.passwordError.observeAsState()
+ var passwordVisible by remember { mutableStateOf(false) }
+
+ OutlinedTextField(
+ value = viewModel.password,
+ onValueChange = {
+ viewModel.onPasswordChanged(it)
+ },
+ label = { Text(stringResource(R.string.password_hint)) },
+ modifier = modifier.fillMaxWidth(),
+ isError = passwordError != null,
+ colors = OutlinedTextFieldDefaults.colors(
+ focusedBorderColor = RhoStrongGray,
+ unfocusedBorderColor = SilverGray,
+ errorBorderColor = ErrorRed,
+ focusedLabelColor = RhoStrongGray,
+ unfocusedLabelColor = TitleGray,
+ focusedLeadingIconColor = RhoStrongGray,
+ unfocusedLeadingIconColor = TitleGray
+ ),
+ supportingText = {
+ if (passwordError != null) {
+ Text(text = passwordError!!)
+ }
+ },
+ leadingIcon = {
+ Icon(imageVector = Icons.Default.Lock, contentDescription = null)
+ },
+ trailingIcon = {
+ val image = if (passwordVisible) Icons.Filled.Visibility else Icons.Filled.VisibilityOff
+ val description = if (passwordVisible) "Hide password" else "Show password"
+
+ IconButton(onClick = { passwordVisible = !passwordVisible }) {
+ Icon(imageVector = image, contentDescription = description)
+ }
+ },
+ visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
+ singleLine = true
+ )
+}
diff --git a/app/src/main/java/com/rho/studio/ui/features/auth/components/LoginPasswordFragment.kt b/app/src/main/java/com/rho/studio/ui/features/auth/components/LoginPasswordFragment.kt
deleted file mode 100644
index bb260d2..0000000
--- a/app/src/main/java/com/rho/studio/ui/features/auth/components/LoginPasswordFragment.kt
+++ /dev/null
@@ -1,95 +0,0 @@
-/**
- * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
- * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
- * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
- * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
- * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
- * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
- *
- * ==============================================================================================
- * File: LoginPasswordFragment.kt
- * Author: Alexis Tercero
- * Email: alexis.tercero@rho.studio
- * Date: 2026-02-23
- * ==============================================================================================
- * Description: Handles email input with two-way binding
- * ==============================================================================================
- */
-package com.rho.studio.ui.features.auth.components
-
-import android.text.Editable
-import android.text.TextWatcher
-import androidx.fragment.app.viewModels
-import androidx.lifecycle.lifecycleScope
-import com.rho.studio.ui.BR
-import com.rho.studio.ui.R
-import com.rho.studio.ui.core.base.BaseFragment
-import com.rho.studio.ui.databinding.FragmentLoginPasswordBinding
-import com.rho.studio.ui.features.auth.LoginViewModel
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-/**
- * LoginPasswordFragment - Reusable password input component
- *
- * Feature: auth
- * Purpose: Handles password input with two-way binding
- *
- * This fragment provides a secure input interface and communicates with the shared
- * [LoginViewModel] to validate password complexity requirements in real-time.
- *
- * ### Key Behaviors:
- * 1. **Shared ViewModel:** Scoped to the parent fragment via [requireParentFragment]
- * to ensure password data is synchronized with email and login action components.
- * 2. **Input Debouncing:** Utilizes a [Job] with a 300ms delay on text changes
- * to optimize performance and prevent UI "stuttering" while the user is typing.
- * 3. **Validation Feedback:** Observes [LoginViewModel.passwordError] to update
- * the [com.google.android.material.textfield.TextInputLayout] error state.
- *
- * ### UI Components:
- * - Uses [FragmentLoginPasswordBinding] to manage the sensitive input field.
- * - Part of the modular `auth` feature components.
- */
-class LoginPasswordFragment : BaseFragment() {
-
- override val viewModel: LoginViewModel by viewModels(ownerProducer = { requireParentFragment() })
- override val layoutId: Int = R.layout.fragment_login_password
- override val bindingVariable: Int = BR.viewModel
-
- private var debounceJob: Job? = null
-
- override fun initializeViews() {
- setupPasswordInput()
- }
-
- override fun setupObservers() {
- viewModel.passwordError.observe(viewLifecycleOwner) { error ->
- withBinding { binding ->
- binding.textInputLayout.error = error
- }
- }
- }
-
- private fun setupPasswordInput() {
- withBinding { binding ->
- binding.passwordInput.addTextChangedListener(object : TextWatcher {
- override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
-
- override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
- debounceJob?.cancel()
- debounceJob = lifecycleScope.launch {
- delay(300)
- viewModel.onPasswordChanged(s?.toString() ?: "")
- }
- }
-
- override fun afterTextChanged(s: Editable?) {}
- })
- }
- }
-
- override fun cleanupBinding() {
- debounceJob?.cancel()
- debounceJob = null
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/rho/studio/ui/features/common/PageFooter.kt b/app/src/main/java/com/rho/studio/ui/features/common/PageFooter.kt
new file mode 100644
index 0000000..5cc1b81
--- /dev/null
+++ b/app/src/main/java/com/rho/studio/ui/features/common/PageFooter.kt
@@ -0,0 +1,59 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ============================================================================
+ * File: PageFooter.kt (composable UI)
+ * Author: Alexis Tercero
+ * Email: alexis.tercero@rho.studio
+ * Date: 2026-07-29
+ * ============================================================================
+ * Description:
+ * A persistent UI component placed at the bottom of the screen to
+ * provide access to global session-level actions.
+ *
+ * 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.
+ * ============================================================================
+ */
+package com.rho.studio.ui.features.common
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+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
+
+@Composable
+fun PageFooter(
+ viewModel: HomeViewModel,
+ modifier: Modifier = Modifier
+) {
+ TextButton(
+ onClick = { viewModel.logout() },
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ colors = ButtonDefaults.textButtonColors(
+ contentColor = colorResource(id = R.color.rho_red)
+ )
+ ) {
+ Text(text = stringResource(id = R.string.logout))
+ }
+}
diff --git a/app/src/main/java/com/rho/studio/ui/features/common/PageFooterFragment.kt b/app/src/main/java/com/rho/studio/ui/features/common/PageFooterFragment.kt
deleted file mode 100644
index 39f9970..0000000
--- a/app/src/main/java/com/rho/studio/ui/features/common/PageFooterFragment.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-/**
- * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
- * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
- * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
- * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
- * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
- * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
- *
- * ==========================================================================
- * File: MainActivity.kt
- * Author: Alexis Tercero
- * Email: alexis.tercero@rho.studio
- * Date: 2026-07-21
- * ==========================================================================
- * Description:
- * A reusable footer fragment for pages.
- * Handles global actions like logout.
- * ==========================================================================
- */
-package com.rho.studio.ui.features.common
-
-import androidx.fragment.app.viewModels
-import com.rho.studio.ui.BR
-import com.rho.studio.ui.R
-import com.rho.studio.ui.core.base.BaseFragment
-import com.rho.studio.ui.databinding.FragmentPageFooterBinding
-import com.rho.studio.ui.features.home.HomeViewModel
-
-class PageFooterFragment : BaseFragment() {
- override val viewModel: HomeViewModel by viewModels(ownerProducer = { requireParentFragment() })
- override val layoutId: Int = R.layout.fragment_page_footer
- override val bindingVariable: Int = BR.viewModel
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/rho/studio/ui/features/common/PageHeader.kt b/app/src/main/java/com/rho/studio/ui/features/common/PageHeader.kt
new file mode 100644
index 0000000..e27dead
--- /dev/null
+++ b/app/src/main/java/com/rho/studio/ui/features/common/PageHeader.kt
@@ -0,0 +1,70 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ============================================================================
+ * File: PageHeader.kt (composable UI)
+ * Author: Alexis Tercero
+ * Email: alexis.tercero@rho.studio
+ * Date: 2026-07-29
+ * ============================================================================
+ * Description:
+ * A standard UI component that provides context and a personalized
+ * greeting at the top of the application's screens.
+ *
+ * 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]").
+ * ============================================================================
+ */
+package com.rho.studio.ui.features.common
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+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.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
+
+@Composable
+fun PageHeader(
+ viewModel: HeaderViewModel,
+ modifier: Modifier = Modifier
+) {
+ val currentUser by viewModel.currentUser.observeAsState()
+ val title by viewModel.title.observeAsState("")
+
+ Column(
+ modifier = modifier.padding(16.dp)
+ ) {
+ Text(
+ text = stringResource(id = R.string.welcome_user, currentUser?.name ?: "User"),
+ fontSize = 24.sp,
+ fontWeight = FontWeight.Bold,
+ color = colorResource(id = R.color.silver_gray)
+ )
+ Text(
+ text = title,
+ fontSize = 16.sp,
+ color = colorResource(id = R.color.silver_gray)
+ )
+ }
+}
diff --git a/app/src/main/java/com/rho/studio/ui/features/common/PageHeaderFragment.kt b/app/src/main/java/com/rho/studio/ui/features/common/PageHeaderFragment.kt
deleted file mode 100644
index 1d90406..0000000
--- a/app/src/main/java/com/rho/studio/ui/features/common/PageHeaderFragment.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-/**
- * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
- * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
- * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
- * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
- * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
- * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
- *
- * ==========================================================================
- * File: PageHeaderFragment.kt
- * Author: Alexis Tercero
- * Email: alexis.tercero@rho.studio
- * Date: 2026-07-21
- * ==========================================================================
- * Description:
- * A reusable header fragment for pages.
- * Uses HeaderViewModel to source user data and accepts a title argument.
- * ==========================================================================
- */
-package com.rho.studio.ui.features.common
-
-import android.os.Bundle
-import androidx.fragment.app.viewModels
-import com.rho.studio.ui.BR
-import com.rho.studio.ui.R
-import com.rho.studio.ui.core.base.BaseFragment
-import com.rho.studio.ui.databinding.FragmentPageHeaderBinding
-
-/**
- * A reusable header fragment for pages.
- * Uses HeaderViewModel to source user data and accepts a title argument.
- */
-class PageHeaderFragment : BaseFragment() {
- override val viewModel: HeaderViewModel by viewModels()
- override val layoutId: Int = R.layout.fragment_page_header
- override val bindingVariable: Int = BR.viewModel
-
- companion object {
- private const val ARG_TITLE = "arg_title"
-
- fun newInstance(title: String): PageHeaderFragment {
- val fragment = PageHeaderFragment()
- val args = Bundle()
- args.putString(ARG_TITLE, title)
- fragment.arguments = args
- return fragment
- }
- }
-
- override fun initializeViews() {
- arguments?.getString(ARG_TITLE)?.let {
- viewModel.setTitle(it)
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/rho/studio/ui/features/home/HomeFragment.kt b/app/src/main/java/com/rho/studio/ui/features/home/HomeFragment.kt
deleted file mode 100644
index 067ac72..0000000
--- a/app/src/main/java/com/rho/studio/ui/features/home/HomeFragment.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-/**
- * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
- * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
- * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
- * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
- * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
- * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
- *
- * ==============================================================================================
- * File: HomeFragment.kt
- * Author: Alexis Tercero
- * Email: alexis.tercero@rho.studio
- * Date: 2026-07-16
- * ==============================================================================================
- * Description: The main dashboard fragment that users land on after successful authentication.
- * Displays module shortcuts and handles global session termination.
- * ==============================================================================================
- */
-package com.rho.studio.ui.features.home
-
-import android.widget.Toast
-import androidx.fragment.app.viewModels
-import com.rho.studio.ui.BR
-import com.rho.studio.ui.R
-import com.rho.studio.ui.core.base.BaseFragment
-import com.rho.studio.ui.databinding.FragmentHomeBinding
-import com.rho.studio.ui.features.home.components.HomeServicesFragment
-import com.rho.studio.ui.features.common.PageFooterFragment
-import com.rho.studio.ui.features.common.PageHeaderFragment
-
-class HomeFragment : BaseFragment() {
-
- override val viewModel: HomeViewModel by viewModels()
- override val layoutId: Int = R.layout.fragment_home
- override val bindingVariable: Int = BR.viewModel
-
- override fun initializeViews() {
- if (isAdded) {
- setupChildFragments()
- }
- }
-
- override fun setupObservers() {
- viewModel.navigateToService.observe(viewLifecycleOwner) { serviceId ->
- serviceId?.let {
- Toast.makeText(context, "Navigating to: $it", Toast.LENGTH_SHORT).show()
- viewModel.onServiceNavigated()
- }
- }
- }
-
- private fun setupChildFragments() {
- childFragmentManager.beginTransaction().apply {
- replace(R.id.header_container, PageHeaderFragment.newInstance(getString(R.string.home_title)))
- replace(R.id.services_container, HomeServicesFragment())
- replace(R.id.footer_container, PageFooterFragment())
- commitAllowingStateLoss()
- }
- }
-}
diff --git a/app/src/main/java/com/rho/studio/ui/features/home/HomeScreen.kt b/app/src/main/java/com/rho/studio/ui/features/home/HomeScreen.kt
new file mode 100644
index 0000000..aff83ae
--- /dev/null
+++ b/app/src/main/java/com/rho/studio/ui/features/home/HomeScreen.kt
@@ -0,0 +1,83 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ============================================================================
+ * File: HomeScreen.kt (composable UI)
+ * Author: Alexis Tercero
+ * Email: alexis.tercero@rho.studio
+ * Date: 2026-07-29
+ * ============================================================================
+ * Description:
+ * The primary landing screen of the application, serving as the main
+ * dashboard 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
+ * HeaderViewModel (navigation/profile) and HomeViewModel (content).
+ * • Modular UI: Composed of reusable building blocks: PageHeader,
+ * ServiceList, and PageFooter.
+ * • 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
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+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.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
+
+@Composable
+fun HomeScreen(
+ homeViewModel: HomeViewModel,
+ headerViewModel: HeaderViewModel = viewModel(),
+ modifier: Modifier = Modifier
+) {
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .background(
+ brush = Brush.verticalGradient(
+ colors = listOf(
+ RhoRed,
+ SilverGray,
+ Black
+ )
+ )
+ )
+ ) {
+ PageHeader(
+ viewModel = headerViewModel,
+ modifier = Modifier.fillMaxWidth()
+ )
+
+ ServiceList(
+ viewModel = homeViewModel,
+ modifier = Modifier.weight(1f)
+ )
+
+ PageFooter(
+ viewModel = homeViewModel,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+}
diff --git a/app/src/main/java/com/rho/studio/ui/features/home/ServiceAdapter.kt b/app/src/main/java/com/rho/studio/ui/features/home/ServiceAdapter.kt
deleted file mode 100644
index b9f7cfe..0000000
--- a/app/src/main/java/com/rho/studio/ui/features/home/ServiceAdapter.kt
+++ /dev/null
@@ -1,64 +0,0 @@
-/**
- * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
- * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
- * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
- * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
- * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
- * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
- *
- * ==========================================================================
- * File: ServiceAdapter.kt
- * Author: Alexis Tercero
- * Email: alexis.tercero@rho.studio
- * Date: 2026-07-21
- * ==========================================================================
- * Description:
- * RecyclerView adapter responsible for managing and displaying ServiceModule
- * items within the Home feature. Utilizes ListAdapter with DiffUtil for
- * optimized list updates and Data Binding to link UI components with the
- * HomeViewModel.
- * ==========================================================================
- */
-package com.rho.studio.ui.features.home
-
-import android.view.LayoutInflater
-import android.view.ViewGroup
-import androidx.recyclerview.widget.DiffUtil
-import androidx.recyclerview.widget.ListAdapter
-import androidx.recyclerview.widget.RecyclerView
-import com.rho.studio.ui.core.model.ServiceModule
-import com.rho.studio.ui.databinding.ItemServiceBinding
-
-class ServiceAdapter(private val viewModel: HomeViewModel) :
- ListAdapter(ServiceDiffCallback()) {
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ServiceViewHolder {
- val layoutInflater = LayoutInflater.from(parent.context)
- val binding = ItemServiceBinding.inflate(layoutInflater, parent, false)
- return ServiceViewHolder(binding)
- }
-
- override fun onBindViewHolder(holder: ServiceViewHolder, position: Int) {
- holder.bind(getItem(position), viewModel)
- }
-
- class ServiceViewHolder(private val binding: ItemServiceBinding) :
- RecyclerView.ViewHolder(binding.root) {
-
- fun bind(service: ServiceModule, viewModel: HomeViewModel) {
- binding.service = service
- binding.viewModel = viewModel
- binding.executePendingBindings()
- }
- }
-
- class ServiceDiffCallback : DiffUtil.ItemCallback() {
- override fun areItemsTheSame(oldItem: ServiceModule, newItem: ServiceModule): Boolean {
- return oldItem.id == newItem.id
- }
-
- override fun areContentsTheSame(oldItem: ServiceModule, newItem: ServiceModule): Boolean {
- return oldItem == newItem
- }
- }
-}
diff --git a/app/src/main/java/com/rho/studio/ui/features/home/components/HomeServicesFragment.kt b/app/src/main/java/com/rho/studio/ui/features/home/components/HomeServicesFragment.kt
deleted file mode 100644
index 39b37f5..0000000
--- a/app/src/main/java/com/rho/studio/ui/features/home/components/HomeServicesFragment.kt
+++ /dev/null
@@ -1,63 +0,0 @@
-/**
- * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
- * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
- * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
- * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
- * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
- * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
- *
- * ==========================================================================
- * File: HomeServicesFragment.kt
- * Author: Alexis Tercero
- * Email: alexis.tercero@rho.studio
- * Date: 2026-07-21
- * ==========================================================================
- * Description:
- * A sub-fragment of the Home feature that implements an MVVM pattern
- * to display a grid of services. It utilizes a shared HomeViewModel
- * to observe service data and populates a RecyclerView via
- * ServiceAdapter. This component leverages ViewBinding and
- * GridLayoutManager to provide a responsive 2-column layout
- * representative of the platform's core offerings.
- * ==========================================================================
- */
-package com.rho.studio.ui.features.home.components
-
-import androidx.fragment.app.viewModels
-import androidx.recyclerview.widget.GridLayoutManager
-import com.rho.studio.ui.BR
-import com.rho.studio.ui.R
-import com.rho.studio.ui.core.base.BaseFragment
-import com.rho.studio.ui.databinding.FragmentHomeServicesBinding
-import com.rho.studio.ui.features.home.HomeViewModel
-import com.rho.studio.ui.features.home.ServiceAdapter
-
-class HomeServicesFragment : BaseFragment() {
-
- override val viewModel: HomeViewModel by viewModels(ownerProducer = { requireParentFragment() })
- override val layoutId: Int = R.layout.fragment_home_services
- override val bindingVariable: Int = BR.viewModel
-
- private lateinit var serviceAdapter: ServiceAdapter
-
- override fun initializeViews() {
- setupRecyclerView()
- }
-
- override fun setupObservers() {
- viewModel.services.observe(viewLifecycleOwner) { services ->
- serviceAdapter.submitList(services)
- }
- }
-
- private fun setupRecyclerView() {
- serviceAdapter = ServiceAdapter(viewModel)
- withBinding { binding ->
- binding.servicesRecyclerView.apply {
- adapter = serviceAdapter
- layoutManager = GridLayoutManager(context, 2)
- setHasFixedSize(true)
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/rho/studio/ui/features/home/components/ServiceItem.kt b/app/src/main/java/com/rho/studio/ui/features/home/components/ServiceItem.kt
new file mode 100644
index 0000000..73bd2dd
--- /dev/null
+++ b/app/src/main/java/com/rho/studio/ui/features/home/components/ServiceItem.kt
@@ -0,0 +1,68 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ============================================================================
+ * File: ServiceItem.kt (composable UI)
+ * Author: Alexis Tercero
+ * Email: alexis.tercero@rho.studio
+ * Date: 2026-07-29
+ * ============================================================================
+ * 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
+ * on the ServiceModule's resource definitions.
+ * • Geometric Design: Features a fixed aspect ratio and rounded
+ * corners to maintain UI consistency across the service grid.
+ * • Localized Content: Automatically resolves and displays title
+ * strings from Android resource IDs.
+ * • Feedback: Built on Material 3 Button semantics to provide
+ * standard touch feedback and accessibility support.
+ * ============================================================================
+ */
+package com.rho.studio.ui.features.home.components
+
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+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.unit.dp
+import com.rho.studio.ui.core.model.ServiceModule
+
+@Composable
+fun ServiceItem(
+ service: ServiceModule,
+ onClick: (String) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Button(
+ onClick = { onClick(service.id) },
+ modifier = modifier
+ .padding(8.dp)
+ .aspectRatio(1f),
+ shape = RoundedCornerShape(16.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = colorResource(id = service.backgroundColor)
+ )
+ ) {
+ Text(
+ text = stringResource(id = service.titleRes),
+ color = Color.White
+ )
+ }
+}
diff --git a/app/src/main/java/com/rho/studio/ui/features/home/components/ServiceList.kt b/app/src/main/java/com/rho/studio/ui/features/home/components/ServiceList.kt
new file mode 100644
index 0000000..267bb3a
--- /dev/null
+++ b/app/src/main/java/com/rho/studio/ui/features/home/components/ServiceList.kt
@@ -0,0 +1,63 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ============================================================================
+ * File: ServiceList.kt (composable UI)
+ * Author: Alexis Tercero
+ * Email: alexis.tercero@rho.studio
+ * Date: 2026-07-29
+ * ============================================================================
+ * 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
+ * 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
+ * the ViewModel for centralized business logic handling.
+ * • Performance: Efficiently renders large lists by utilizing
+ * lazy-loading mechanics.
+ * ============================================================================
+ */
+package com.rho.studio.ui.features.home.components
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+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.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
+
+@Composable
+fun ServiceList(
+ viewModel: HomeViewModel,
+ modifier: Modifier = Modifier
+) {
+ val services by viewModel.services.observeAsState(emptyList())
+
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(2),
+ modifier = modifier.fillMaxSize(),
+ contentPadding = PaddingValues(8.dp)
+ ) {
+ items(services) { service ->
+ ServiceItem(
+ service = service,
+ onClick = { viewModel.onServiceClick(it) }
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/com/rho/studio/ui/ui/theme/Color.kt b/app/src/main/java/com/rho/studio/ui/ui/theme/Color.kt
index b28a9d4..f35f95d 100644
--- a/app/src/main/java/com/rho/studio/ui/ui/theme/Color.kt
+++ b/app/src/main/java/com/rho/studio/ui/ui/theme/Color.kt
@@ -8,4 +8,16 @@ val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
-val Pink40 = Color(0xFF7D5260)
\ No newline at end of file
+val Pink40 = Color(0xFF7D5260)
+
+// Brand Colors
+val BackgroundLightGray = Color(0xFFD3D3D3)
+val TitleGray = Color(0xFF696969)
+val DarkGray = Color(0xFF4A4A4A)
+val Black = Color(0xFF000000)
+val White = Color(0xFFFFFFFF)
+val RhoRed = Color(0xFFD32F2F)
+val RhoStrongGray = Color(0xFF333333)
+val ErrorRed = Color(0xFFFF0000)
+val SuccessGreen = Color(0xFF4CAF50)
+val SilverGray = Color(0xFFC0C0C0)
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
deleted file mode 100644
index 4fcf6d0..0000000
--- a/app/src/main/res/layout/activity_main.xml
+++ /dev/null
@@ -1,58 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml
deleted file mode 100644
index a3faac7..0000000
--- a/app/src/main/res/layout/fragment_home.xml
+++ /dev/null
@@ -1,56 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_home_services.xml b/app/src/main/res/layout/fragment_home_services.xml
deleted file mode 100644
index cabed5e..0000000
--- a/app/src/main/res/layout/fragment_home_services.xml
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_login.xml b/app/src/main/res/layout/fragment_login.xml
deleted file mode 100644
index bc775bc..0000000
--- a/app/src/main/res/layout/fragment_login.xml
+++ /dev/null
@@ -1,71 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_login_button.xml b/app/src/main/res/layout/fragment_login_button.xml
deleted file mode 100644
index 02ba0d2..0000000
--- a/app/src/main/res/layout/fragment_login_button.xml
+++ /dev/null
@@ -1,52 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_login_email.xml b/app/src/main/res/layout/fragment_login_email.xml
deleted file mode 100644
index 22112a8..0000000
--- a/app/src/main/res/layout/fragment_login_email.xml
+++ /dev/null
@@ -1,50 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_login_password.xml b/app/src/main/res/layout/fragment_login_password.xml
deleted file mode 100644
index 9d67e6f..0000000
--- a/app/src/main/res/layout/fragment_login_password.xml
+++ /dev/null
@@ -1,50 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_page_footer.xml b/app/src/main/res/layout/fragment_page_footer.xml
deleted file mode 100644
index 535ed35..0000000
--- a/app/src/main/res/layout/fragment_page_footer.xml
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_page_header.xml b/app/src/main/res/layout/fragment_page_header.xml
deleted file mode 100644
index 62c9643..0000000
--- a/app/src/main/res/layout/fragment_page_header.xml
+++ /dev/null
@@ -1,60 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_service.xml b/app/src/main/res/layout/item_service.xml
deleted file mode 100644
index e5a9813..0000000
--- a/app/src/main/res/layout/item_service.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml
deleted file mode 100644
index 11b92f9..0000000
--- a/app/src/main/res/navigation/nav_graph.xml
+++ /dev/null
@@ -1,49 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 101a1d6..e17f771 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -10,8 +10,10 @@ junit = "4.13.2"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
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"
@@ -24,8 +26,11 @@ junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
+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" }
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
@@ -33,6 +38,7 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
+androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigation" }
androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigation" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }