diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
new file mode 100644
index 0000000..1af7126
--- /dev/null
+++ b/.github/workflows/android.yml
@@ -0,0 +1,35 @@
+name: Android CI/CD Rho.Studio®
+
+on:
+ workflow_dispatch:
+ push:
+ branches: [ "pre-release" ]
+ pull_request:
+ branches: [ "main" ]
+
+jobs:
+ build:
+ name: Build and Test Rho.Studio®
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@v6.1.0
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v5.5.0
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+ cache: gradle
+
+ - name: Grant execute permission for gradlew
+ run: chmod +x gradlew
+ - name: Build with Gradle
+ run: ./gradlew build
+
+ - name: Upload APK
+ uses: actions/upload-artifact@v7.0.1
+ with:
+ name: app-debug
+ path: app/build/outputs/apk/debug/app-debug.apk
diff --git a/.gitignore b/.gitignore
index 14e5685..8773f8b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,3 +27,6 @@ replay_pid*
.kotlin/
*.lock
*.bin
+.idea/
+.gradle/
+build/
diff --git a/.gradle/8.13/gc.properties b/.gradle/8.13/gc.properties
deleted file mode 100644
index e69de29..0000000
diff --git a/.gradle/buildOutputCleanup/cache.properties b/.gradle/buildOutputCleanup/cache.properties
deleted file mode 100644
index 67d56c5..0000000
--- a/.gradle/buildOutputCleanup/cache.properties
+++ /dev/null
@@ -1,2 +0,0 @@
-#Wed Nov 19 15:12:11 CST 2025
-gradle.version=8.13
diff --git a/.gradle/config.properties b/.gradle/config.properties
deleted file mode 100644
index f857839..0000000
--- a/.gradle/config.properties
+++ /dev/null
@@ -1,2 +0,0 @@
-#Wed Nov 19 15:11:51 CST 2025
-java.home=C\:\\Program Files\\Android\\Android Studio\\jbr
diff --git a/.gradle/file-system.probe b/.gradle/file-system.probe
deleted file mode 100644
index 0101824..0000000
Binary files a/.gradle/file-system.probe and /dev/null differ
diff --git a/.gradle/vcs-1/gc.properties b/.gradle/vcs-1/gc.properties
deleted file mode 100644
index e69de29..0000000
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 26d3352..0000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml
deleted file mode 100644
index 4a53bee..0000000
--- a/.idea/AndroidProjectSystem.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/caches/deviceStreaming.xml b/.idea/caches/deviceStreaming.xml
deleted file mode 100644
index 4390067..0000000
--- a/.idea/caches/deviceStreaming.xml
+++ /dev/null
@@ -1,1390 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
deleted file mode 100644
index b86273d..0000000
--- a/.idea/compiler.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
deleted file mode 100644
index b268ef3..0000000
--- a/.idea/deploymentTargetSelector.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml
deleted file mode 100644
index 91f9558..0000000
--- a/.idea/deviceManager.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
deleted file mode 100644
index 639c779..0000000
--- a/.idea/gradle.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
deleted file mode 100644
index 7061a0d..0000000
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ /dev/null
@@ -1,61 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/migrations.xml b/.idea/migrations.xml
deleted file mode 100644
index f8051a6..0000000
--- a/.idea/migrations.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index b2c751a..0000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
deleted file mode 100644
index 16660f1..0000000
--- a/.idea/runConfigurations.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 94a25f7..0000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/README.md b/README.md
index 3f19be7..29087e0 100644
--- a/README.md
+++ b/README.md
@@ -1,55 +1,18 @@
-# UI-Utils-Rho-Studio
-Reusable fragments
+# Rho Studio UI App
+#### An Android View System app.
-```mermaid
-classDiagram
- class Credentials {
- +String email
- +String password
- +Constructor(email: String, password: String)
- }
+
- class LoginViewModel {
- -Credentials credentialslive
- -String successMessage
- -String errorMessage
- +String? toastMessage
- +String? userEmail
- +String? userPassword
- +onButtonClicked()
- -isValid(): Boolean
- }
+## Architecture
- class BaseObservable {
- <>
- +notifyPropertyChanged()
- }
+- MVVM Architecture for managing the app and code.
+- Single-activity Android architecture.
+- Android View System for UI.
+- Session manager.
- class BR {
- <>
- +Int userEmail
- +Int userPassword
- +Int toastMessage
- }
+### Current navigation graph
- class ActivityMainBinding {
- +LoginViewModel loginViewModel
- +executePendingBindings()
- }
+
- class MainActivity {
- +onCreate()
- -ActivityMainBinding activityMainBinding
- }
- class BindingAdapters {
- +runMe(view: View, message: String?)
- }
-
- LoginViewModel --|> BaseObservable
- LoginViewModel *-- Credentials
- LoginViewModel ..> BR : uses
- MainActivity --> ActivityMainBinding
- ActivityMainBinding --> LoginViewModel
- BindingAdapters --> LoginViewModel : observes toastMessage
-```
\ No newline at end of file
+[**Rho.Studio®**](https://rho.studio/)
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 312898c..b26cc37 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,18 +1,18 @@
plugins {
alias(libs.plugins.android.application)
- alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
- id("kotlin-kapt")
+ alias(libs.plugins.kotlin.parcelize)
+ alias(libs.plugins.legacy.kapt)
}
android {
namespace = "com.rho.studio.ui"
- compileSdk = 36
+ compileSdk = 37
defaultConfig {
applicationId = "com.rho.studio.ui"
- minSdk = 21
- targetSdk = 36
+ minSdk = 23
+ targetSdk = 37
versionCode = 1
versionName = "1.0"
@@ -29,15 +29,36 @@ android {
}
}
compileOptions {
- sourceCompatibility = JavaVersion.VERSION_11
- targetCompatibility = JavaVersion.VERSION_11
- }
- kotlinOptions {
- jvmTarget = "11"
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
}
+
buildFeatures {
compose = true
dataBinding = true
+ viewBinding = true // Optional but recommended
+ }
+
+ sourceSets {
+ getByName("main") {
+ // Java/Kotlin source directories
+ java.srcDirs(
+ "src/main/java" // This includes everything under java/
+ // No need to list each feature separately
+ )
+
+ // Resource directories - THIS IS KEY FOR FEATURE RESOURCES
+ res.srcDirs(
+ "src/main/res" // Global resources
+ )
+ }
+ }
+}
+
+// Add the new DSL here
+kotlin {
+ compilerOptions {
+ jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
}
@@ -51,6 +72,11 @@ dependencies {
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.gson)
+ implementation(libs.material)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
diff --git a/app/sampledata/services.json b/app/sampledata/services.json
new file mode 100644
index 0000000..bab6b8d
--- /dev/null
+++ b/app/sampledata/services.json
@@ -0,0 +1,18 @@
+[
+ {
+ "id": "S1",
+ "name": "Inventory"
+ },
+ {
+ "id": "S2",
+ "name": "Sales"
+ },
+ {
+ "id": "S3",
+ "name": "Customers"
+ },
+ {
+ "id": "S4",
+ "name": "Reports"
+ }
+]
diff --git a/app/sampledata/services.txt b/app/sampledata/services.txt
new file mode 100644
index 0000000..0f9ef15
--- /dev/null
+++ b/app/sampledata/services.txt
@@ -0,0 +1,6 @@
+S1
+S2
+S3
+S4
+S5
+S6
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index cab9de0..509af97 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -10,15 +10,13 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
- android:theme="@style/Theme.UI">
+ android:theme="@style/Theme.RhoStudio">
+ android:launchMode="singleTop">
-
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 53f2ae7..6faf781 100644
--- a/app/src/main/java/com/rho/studio/ui/MainActivity.kt
+++ b/app/src/main/java/com/rho/studio/ui/MainActivity.kt
@@ -1,4 +1,4 @@
-/*
+/**
* ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
* ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
* ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
@@ -10,39 +10,192 @@
* File: MainActivity.kt
* Author: Alexis Tercero
* Email: alexis.tercero@rho.studio
- * Date: 2026-02-12
+ * Date: 2026-07-20
* ==========================================================================
* Description:
- * MVVM Login, min activity with data binding.
+ * This activity follows the "Single Activity" architecture pattern, acting as the
+ * main orchestrator for fragment navigation and global state management.
* ==========================================================================
*/
package com.rho.studio.ui
import android.os.Bundle
+import android.util.Log
import android.view.View
import android.widget.Toast
-import androidx.activity.ComponentActivity
-
-import androidx.databinding.BindingAdapter
+import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
+import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.NavController
+import androidx.navigation.fragment.NavHostFragment
+import com.rho.studio.ui.core.manager.SessionManager
import com.rho.studio.ui.databinding.ActivityMainBinding
-import com.rho.studio.ui.viewmodel.LoginViewModel
+import com.rho.studio.ui.features.auth.LoginFragment
+import com.rho.studio.ui.features.auth.LoginViewModel
+import com.rho.studio.ui.features.home.HomeFragment
+
+/**
+ * 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() {
+
+ private lateinit var binding: ActivityMainBinding
+ private lateinit var sessionManager: SessionManager
+ private lateinit var loginViewModel: LoginViewModel
+ private lateinit var navController: NavController
+ private var isNavGraphReady = false
-@BindingAdapter("toastMessage")
-fun runMe(view: View, message: String?) { // Added ? for null safety
- if (!message.isNullOrEmpty()) { // Checked for null/empty
- Toast.makeText(view.context, message, Toast.LENGTH_SHORT).show()
- }
-}
-class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- // ViewModel updates the Model after observing changes in the View
- // Model will also update the View via the ViewModel
- val activityMainBinding: ActivityMainBinding =
- DataBindingUtil.setContentView(this, R.layout.activity_main)
- activityMainBinding.loginViewModel = LoginViewModel()
- activityMainBinding.executePendingBindings()
+ 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()
+
+ // 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()
+ }
}
-}
\ No newline at end of file
+
+ 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
+ }
+ }
+
+ sessionManager.error.observe(this) { error ->
+ error?.let {
+ android.widget.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)
+ }
+ }
+
+ private fun showHomeScreen() {
+ val currentDest = navController.currentDestination?.id
+ if (currentDest != null && currentDest != R.id.homeFragment) {
+ navController.navigate(R.id.action_loginFragment_to_homeFragment)
+ }
+ }
+
+ 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 {
+ if (isAuthenticated) {
+ showHomeScreen()
+ loginViewModel.resetForm()
+ } else {
+ showLoginScreen()
+ }
+ }
+ }
+
+ /**
+ * 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)
+
+ // Choose start destination based on authentication state
+ graph.setStartDestination(if (isAuthenticated) R.id.homeFragment else R.id.loginFragment)
+
+ navController.graph = graph
+ }
+
+ fun getLoginViewModel(): LoginViewModel = loginViewModel
+
+ override fun onDestroy() {
+ super.onDestroy()
+ sessionManager.cleanup()
+ }
+}
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
new file mode 100644
index 0000000..18aaa72
--- /dev/null
+++ b/app/src/main/java/com/rho/studio/ui/core/base/BaseFragment.kt
@@ -0,0 +1,220 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ==============================================================================================
+ * 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/core/base/BaseViewModel.kt b/app/src/main/java/com/rho/studio/ui/core/base/BaseViewModel.kt
new file mode 100644
index 0000000..04175eb
--- /dev/null
+++ b/app/src/main/java/com/rho/studio/ui/core/base/BaseViewModel.kt
@@ -0,0 +1,171 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ==============================================================================================
+ * File: BaseViewModel.kt
+ * Author: Alexis Tercero
+ * Email: alexis.tercero@rho.studio
+ * Date: 2026-07-15
+ * ==============================================================================================
+ * Description:
+ * The BaseViewModel is an abstract base class designed for the Rho Studio UI architecture.
+ * It extends the standard Android androidx.lifecycle.ViewModel to provide a consistent
+ * foundation for state management, coroutine safety, and error handling across
+ * all feature-specific ViewModels.
+ *
+ * Key Features
+ * •Automatic Loading State: Integrated tracking of background tasks.
+ * •Safe Coroutine Execution: Built-in exception handling to prevent app crashes.
+ * •Job Management: Automatic cancellation of active coroutines when the ViewModel is cleared.
+ * •UI Communication: Standardized LiveData streams for errors and toast notifications.
+ * ==============================================================================================
+ */
+package com.rho.studio.ui.core.base
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+
+/**
+ * ## BaseViewModel - Base class for all feature ViewModels
+ *
+ * Abstract base class designed for the Rho Studio UI architecture.
+ * It extends the standard Android androidx.lifecycle.ViewModel to provide a consistent
+ * foundation for state management, coroutine safety, and error handling across
+ * all feature-specific ViewModels.
+ *
+ * __Key Features__
+ *
+ * •Automatic Loading State: Integrated tracking of background tasks.
+ *
+ * •Safe Coroutine Execution: Built-in exception handling to prevent app crashes.
+ *
+ * •Job Management: Automatic cancellation of active coroutines when the ViewModel is cleared.
+ *
+ * •UI Communication: Standardized LiveData streams for errors and toast notifications.
+ *
+ * In standard Android development, if a coroutine launched in viewModelScope
+ * throws an exception that isn't caught, the entire app crashes.By using launchSafe,
+ * avoiding repetitive boilerplate code. Instead of writing try-catch in every single function
+ */
+abstract class BaseViewModel : ViewModel() {
+
+ // Loading state
+ private val _isLoading = MutableLiveData(false)
+ val isLoading: LiveData = _isLoading
+
+ // Error messages
+ private val _error = MutableLiveData()
+ val error: LiveData = _error
+
+ // Toast messages (one-time)
+ private val _toastMessage = MutableLiveData()
+ val toastMessage: LiveData = _toastMessage
+
+ // Track active jobs
+ private val jobs = mutableListOf()
+
+ /**
+ * Launch a coroutine with automatic loading state
+ */
+ protected fun launchWithLoading(
+ block: suspend CoroutineScope.() -> Unit,
+ onError: ((Exception) -> Unit)? = null
+ ): Job {
+ _isLoading.postValue(true)
+
+ val job = viewModelScope.launch {
+ try {
+ block()
+ } catch (e: Exception) {
+ handleError(e)
+ onError?.invoke(e)
+ } finally {
+ _isLoading.postValue(false)
+ }
+ }
+
+ jobs.add(job)
+ return job
+ }
+
+ /**
+ * Executes a coroutine block safely within the [viewModelScope].
+ *
+ * This function wraps the execution of the [block] in a try-catch block.
+ * If an exception occurs during the execution of the coroutine, it is caught
+ * and passed to [handleError], preventing the app from crashing.
+ *
+ * @param block The suspendable lambda expression to be executed.
+ */
+ protected fun launchSafe(
+ block: suspend CoroutineScope.() -> Unit,
+ onError: ((Exception) -> Unit)? = null
+ ): Job {
+ val job = viewModelScope.launch {
+ try {
+ block()
+ } catch (e: Exception) {
+ handleError(e)
+ onError?.invoke(e)
+ }
+ }
+
+ jobs.add(job)
+ return job
+ }
+
+ // ==================== ERROR HANDLING ====================
+ /**
+ * Centralized error handling logic for coroutines launched via [launchSafe].
+ *
+ * The default implementation prints the stack trace. Override this method
+ * in subclasses to provide feature-specific error handling, such as updating
+ * UI state with error messages or logging to a remote service.
+ *
+ * @param e The [Exception] caught during coroutine execution.
+ */
+ protected open fun handleError(e: Exception) {
+ e.printStackTrace()
+ _error.postValue(e.message ?: "An error occurred")
+ }
+
+ /** Clear current error message */
+ fun clearError() {
+ _error.value = null
+ }
+
+ // ==================== TOAST MESSAGES ====================
+
+ /** Show a one-time toast message to user */
+ protected fun showToast(message: String) {
+ _toastMessage.postValue(message)
+ }
+
+ /** Clear current toast message */
+ fun clearToastMessage() {
+ _toastMessage.value = null
+ }
+
+ // ==================== JOB MANAGEMENT ====================
+
+ /** Cancel all active coroutine jobs */
+ fun cancelAllJobs() {
+ jobs.forEach { it.cancel() }
+ jobs.clear()
+ }
+
+ override fun onCleared() {
+ cancelAllJobs() // Prevent memory leaks
+ //super.onCleared()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/rho/studio/ui/core/manager/SessionManager.kt b/app/src/main/java/com/rho/studio/ui/core/manager/SessionManager.kt
new file mode 100644
index 0000000..50cba10
--- /dev/null
+++ b/app/src/main/java/com/rho/studio/ui/core/manager/SessionManager.kt
@@ -0,0 +1,237 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ==============================================================================================
+ * File: SessionManager.kt
+ * Author: Alexis Tercero
+ * Email: alexis.tercero@rho.studio
+ * Date: 2026-07-14
+ * ==============================================================================================
+ * Description: Singleton orchestrator for authentication state and session lifecycle.
+ * Delegates persistence to a SessionRepository
+ * and exposes reactive state via LiveData.
+ * ==============================================================================================
+ */
+package com.rho.studio.ui.core.manager
+
+import android.content.Context
+import android.util.Log
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.rho.studio.ui.core.model.User
+import com.rho.studio.ui.core.repository.SessionRepository
+import com.rho.studio.ui.core.repository.SessionRepositoryImpl
+import kotlinx.coroutines.*
+
+/**
+ * # SessionManager
+ * Core module - Singleton orchestrator for authentication state and session lifecycle.
+ * Delegates persistence to a SessionRepository and exposes reactive state via LiveData.
+ */
+class SessionManager private constructor() {
+
+ companion object {
+ private const val TAG = "SessionManager"
+
+ /** ensure that changes to the instance variable in a SessionManager are
+ * immediately visible to all threads, preventing issues caused
+ * by thread-local caching.*/
+ @Volatile
+ private var instance: SessionManager? = null
+
+ /**
+ * Thread-safe singleton instance getter.
+ */
+ fun getInstance(): SessionManager {
+ return instance ?: synchronized(this) {
+ instance ?: SessionManager().also { instance = it }
+ }
+ }
+
+ /**
+ * Initialize the session manager with application context.
+ * Should be called in Application.onCreate() or MainActivity.onCreate().
+ */
+ fun init(context: Context) {
+ getInstance().initialize(SessionRepositoryImpl(context))
+ }
+ }
+
+ // ==================== PROPERTIES ====================
+
+ /**Instead of blocking the UI thread when saving
+ * to disk or simulating a network call,
+ * it uses a dedicated sessionScope*/
+ private val sessionScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
+ private lateinit var repository: SessionRepository
+ private var isInitialized = false
+
+ /** # Private mutable LiveData
+ * Only the SessionManager can modify the state*/
+ private val _isAuthenticated = MutableLiveData(false)
+ private val _currentUser = MutableLiveData(null)
+ private val _isLoading = MutableLiveData(false)
+ private val _error = MutableLiveData(null)
+ private val _isSessionChecked = MutableLiveData(false)
+
+ /** # PUBLIC LIVEDATA
+ * The rest of the app (UI/ViewModels) can only observe the state.*/
+ val isAuthenticated: LiveData = _isAuthenticated
+ val currentUser: LiveData = _currentUser
+ val isLoading: LiveData = _isLoading
+ val error: LiveData = _error
+ val isSessionChecked: LiveData = _isSessionChecked
+
+ // ==================== INITIALIZATION ====================
+
+ private fun initialize(repository: SessionRepository) {
+ if (isInitialized) return
+
+ this.repository = repository
+ isInitialized = true
+ loadSavedSession()
+ }
+
+ private fun checkInitialized() {
+ if (!isInitialized) {
+ throw IllegalStateException("SessionManager must be initialized with init(context) before use.")
+ }
+ }
+
+ // ==================== PUBLIC METHODS ====================
+
+ /**
+ * Authenticate user with credentials (mock implementation).
+ */
+ fun login(email: String, password: String): Job {
+ checkInitialized()
+ _isLoading.postValue(true)
+ _error.postValue(null)
+
+ return sessionScope.launch {
+ try {
+ delay(1500) // Simulate network
+
+ if (isValidCredentials(email, password)) {
+ val user = User(
+ id = "user_${System.currentTimeMillis()}",
+ email = email,
+ name = extractNameFromEmail(email)
+ )
+
+ saveUserSession(user)
+
+ withContext(Dispatchers.Main) {
+ _currentUser.value = user
+ _isAuthenticated.value = true
+ }
+ } else {
+ _error.postValue("Invalid email or password")
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Login error", e)
+ _error.postValue("Login failed: ${e.message}")
+ } finally {
+ _isLoading.postValue(false)
+ }
+ }
+ }
+
+ fun logout(): Job {
+ checkInitialized()
+ _isLoading.postValue(true)
+
+ return sessionScope.launch {
+ try {
+ clearUserSession()
+ withContext(Dispatchers.Main) {
+ _currentUser.value = null
+ _isAuthenticated.value = false
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Logout error", e)
+ } finally {
+ _isLoading.postValue(false)
+ }
+ }
+ }
+
+ fun isAuthenticatedSync(): Boolean = _isAuthenticated.value ?: false
+
+ fun getCurrentUserSync(): User? = _currentUser.value
+
+ fun clearError() {
+ _error.value = null
+ }
+
+ // ==================== PRIVATE HELPERS ====================
+
+ private fun isValidCredentials(email: String, password: String): Boolean {
+ return email.isNotBlank() &&
+ android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches() &&
+ password.length >= 6
+ }
+
+ private fun extractNameFromEmail(email: String): String {
+ return email.substringBefore("@")
+ .split(".", "_", "-")
+ .joinToString(" ") { it.replaceFirstChar { c -> c.uppercase() } }
+ }
+
+ private fun loadSavedSession() {
+ sessionScope.launch {
+ try {
+ val user = repository.getUser()
+ withContext(Dispatchers.Main) {
+ if (user != null) {
+ Log.d(TAG, "Found saved session for: ${user.email}")
+ _currentUser.value = user
+ _isAuthenticated.value = true
+ } else {
+ Log.d(TAG, "No saved session found")
+ _isAuthenticated.value = false
+ }
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to load saved user", e)
+ withContext(Dispatchers.Main) {
+ _isAuthenticated.value = false
+ }
+ clearUserSession()
+ } finally {
+ withContext(Dispatchers.Main) {
+ _isSessionChecked.value = true
+ }
+ }
+ }
+ }
+
+ private fun saveUserSession(user: User) {
+ sessionScope.launch {
+ try {
+ repository.saveUser(user)
+ } catch (e: Exception) {
+ Log.e(TAG, "Error saving session", e)
+ }
+ }
+ }
+
+ private fun clearUserSession() {
+ sessionScope.launch {
+ try {
+ repository.clearSession()
+ } catch (e: Exception) {
+ Log.e(TAG, "Error clearing session", e)
+ }
+ }
+ }
+
+ fun cleanup() {
+ sessionScope.cancel()
+ }
+}
diff --git a/app/src/main/java/com/rho/studio/ui/core/model/Credentials.kt b/app/src/main/java/com/rho/studio/ui/core/model/Credentials.kt
new file mode 100644
index 0000000..877d371
--- /dev/null
+++ b/app/src/main/java/com/rho/studio/ui/core/model/Credentials.kt
@@ -0,0 +1,69 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ==============================================================================================
+ * File: Credentials.kt
+ * Author: Alexis Tercero
+ * Email: alexis.tercero@rho.studio
+ * Date: 2026-02-23
+ * ==============================================================================================
+ * Description:
+ * Represents the user's authentication data and provides validation logic.
+ * ==============================================================================================
+ */
+package com.rho.studio.ui.core.model
+
+import android.util.Patterns
+
+/**
+ * Represents the user's authentication data and provides validation logic.
+ *
+ * This class encapsulates the email and password fields, offering helper properties
+ * to verify format integrity (RFC-compliant email patterns) and security requirements
+ * (minimum password length).
+ *
+ * It is primarily used by ViewModels to manage UI state and provide immediate
+ * validation feedback to the user.
+ *
+ * @property email The user's email address. Defaults to an empty string.
+ * @property password The user's password. Defaults to an empty string.
+ */
+data class Credentials(
+ var email: String = "",
+ var password: String = ""
+) {
+ val isEmailValid: Boolean
+ get() = email.isNotBlank() && Patterns.EMAIL_ADDRESS.matcher(email).matches()
+
+ val isPasswordValid: Boolean
+ get() = password.length >= 6
+
+ val isValid: Boolean
+ get() = isEmailValid && isPasswordValid
+
+ val validationErrors: List
+ get() {
+ val errors = mutableListOf()
+ if (email.isBlank()) errors.add("Email is required")
+ else if (!isEmailValid) errors.add("Invalid email format")
+
+ if (password.isBlank()) errors.add("Password is required")
+ else if (!isPasswordValid) errors.add("Password must be at least 6 characters")
+
+ return errors
+ }
+
+ val isEmpty: Boolean
+ get() = email.isEmpty() && password.isEmpty()
+
+ fun clear() {
+ email = ""
+ password = ""
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/rho/studio/ui/core/model/ServiceModule.kt b/app/src/main/java/com/rho/studio/ui/core/model/ServiceModule.kt
new file mode 100644
index 0000000..5e64b8e
--- /dev/null
+++ b/app/src/main/java/com/rho/studio/ui/core/model/ServiceModule.kt
@@ -0,0 +1,16 @@
+package com.rho.studio.ui.core.model
+
+import androidx.annotation.ColorRes
+
+/**
+ * Represents a service or module available on the home dashboard.
+ *
+ * @property id Unique identifier for the service.
+ * @property titleRes String resource ID for the service name.
+ * @property backgroundColor Background color resource for the button.
+ */
+data class ServiceModule(
+ val id: String,
+ val titleRes: Int,
+ @ColorRes val backgroundColor: Int
+)
diff --git a/app/src/main/java/com/rho/studio/ui/core/model/User.kt b/app/src/main/java/com/rho/studio/ui/core/model/User.kt
new file mode 100644
index 0000000..1148ff4
--- /dev/null
+++ b/app/src/main/java/com/rho/studio/ui/core/model/User.kt
@@ -0,0 +1,114 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ==============================================================================================
+ * File: User.kt
+ * Author: Alexis Tercero
+ * Email: alexis.tercero@rho.studio
+ * Date: 2026-02-23
+ * ==============================================================================================
+ * Description: User model - Core data class used across features
+ * ==============================================================================================
+ */
+package com.rho.studio.ui.core.model
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+/**
+ * User model - Core data class used across features
+ *
+ * This class implements [Parcelable] via `@Parcelize` to allow efficient data
+ * transfer between Android components (Activities, Fragments) and to survive
+ * configuration changes (screen rotations).
+ *
+ * It encapsulates basic identity data, profile information, and nested
+ * [UserPreferences].
+ *
+ * @property id Unique identifier for the user.
+ * @property email Primary contact and login identifier.
+ * @property name Full name of the user. Defaults to empty string.
+ * @property avatar Optional URL or path to the user's profile image.
+ * @property createdAt Epoch timestamp of account creation.
+ * @property lastLogin Epoch timestamp of the most recent session.
+ * @property preferences Nested settings and UI configurations.
+ * @property isActive Flag indicating if the account is currently enabled.
+ */
+@Parcelize
+data class User(
+ val id: String,
+ val email: String,
+ val name: String = "",
+ val avatar: String? = null,
+ val createdAt: Long = System.currentTimeMillis(),
+ val lastLogin: Long = System.currentTimeMillis(),
+ val preferences: UserPreferences = UserPreferences(),
+ val isActive: Boolean = true
+) : Parcelable {
+
+ val initials: String
+ get() = if (name.isNotEmpty()) {
+ name.split(" ")
+ .take(2)
+ .mapNotNull { it.firstOrNull()?.toString() }
+ .joinToString("")
+ .uppercase()
+ } else {
+ email.take(2).uppercase()
+ }
+
+ val displayName: String
+ get() = if (name.isNotEmpty()) name else email.substringBefore("@")
+
+ fun getFormattedCreatedAt(pattern: String = "MMM dd, yyyy"): String {
+ return SimpleDateFormat(pattern, Locale.getDefault()).format(Date(createdAt))
+ }
+
+ fun getLastLoginRelative(): String {
+ val now = System.currentTimeMillis()
+ val diff = now - lastLogin
+
+ return when {
+ diff < 60_000 -> "Just now"
+ diff < 3_600_000 -> "${diff / 60_000} minutes ago"
+ diff < 86_400_000 -> "${diff / 3_600_000} hours ago"
+ diff < 2_592_000_000 -> "${diff / 86_400_000} days ago"
+ else -> "Long time ago"
+ }
+ }
+}
+/**
+* Consider splitting if:
+* UserPreferences grows significantly (adds 10+ properties)
+* Other models depend on UserPreferences (creating circular dependencies)
+* File exceeds 200-300 lines
+* Different teams own User and UserPreferences
+* */
+@Parcelize
+data class UserPreferences(
+ val darkMode: Boolean = false,
+ val notificationsEnabled: Boolean = true,
+ val language: String = "en"
+) : Parcelable
+
+/**
+ * User model - Core data class used across features
+ *
+ * In Android, you cannot simply pass a custom Kotlin object
+ * (like User) directly from one Activity to another or
+ * save it when the screen rotates.
+ * The data must be converted into a format the Android
+ * System understands (a byte stream).
+ * This process is called Serialization.
+ *
+ * The standard way to do this in Android
+ * is by implementing the Parcelable interface.
+ */
\ No newline at end of file
diff --git a/app/src/main/java/com/rho/studio/ui/core/repository/SessionRepository.kt b/app/src/main/java/com/rho/studio/ui/core/repository/SessionRepository.kt
new file mode 100644
index 0000000..3cc118e
--- /dev/null
+++ b/app/src/main/java/com/rho/studio/ui/core/repository/SessionRepository.kt
@@ -0,0 +1,71 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ==============================================================================================
+ * File: SessionRepository.kt
+ * Author: Alexis Tercero
+ * Email: alexis.tercero@rho.studio
+ * Date: 2026-07-14
+ * ==============================================================================================
+ * Description: Repository for persisting session data.
+ * ==============================================================================================
+ */
+package com.rho.studio.ui.core.repository
+
+import android.content.Context
+import android.content.SharedPreferences
+import androidx.core.content.edit
+import com.google.gson.Gson
+import com.rho.studio.ui.core.model.User
+
+/**
+ * Interface defining the persistence operations for user sessions.
+ */
+interface SessionRepository {
+ suspend fun saveUser(user: User)
+ suspend fun getUser(): User?
+ suspend fun clearSession()
+}
+
+/**
+ * Implementation of [SessionRepository] using SharedPreferences.
+ */
+class SessionRepositoryImpl(context: Context) : SessionRepository {
+
+ private val preferences: SharedPreferences =
+ context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
+
+ private val gson = Gson()
+
+ companion object {
+ private const val PREFS_NAME = "session_prefs"
+ private const val PREF_KEY_USER = "pref_current_user"
+ }
+
+ override suspend fun saveUser(user: User) {
+ val userJson = gson.toJson(user)
+ preferences.edit {
+ putString(PREF_KEY_USER, userJson)
+ }
+ }
+
+ override suspend fun getUser(): User? {
+ val userJson = preferences.getString(PREF_KEY_USER, null) ?: return null
+ return try {
+ gson.fromJson(userJson, User::class.java)
+ } catch (e: Exception) {
+ null
+ }
+ }
+
+ override suspend fun clearSession() {
+ preferences.edit {
+ remove(PREF_KEY_USER)
+ }
+ }
+}
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
new file mode 100644
index 0000000..c0b37b6
--- /dev/null
+++ b/app/src/main/java/com/rho/studio/ui/features/auth/LoginFragment.kt
@@ -0,0 +1,91 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ==============================================================================================
+ * 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/LoginViewModel.kt b/app/src/main/java/com/rho/studio/ui/features/auth/LoginViewModel.kt
new file mode 100644
index 0000000..52e3434
--- /dev/null
+++ b/app/src/main/java/com/rho/studio/ui/features/auth/LoginViewModel.kt
@@ -0,0 +1,173 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ============================================================================
+ * File: LoginViewModel.kt
+ * Author: Alexis Tercero
+ * Email: alexis.tercero@rho.studio
+ * Date: 2026-07-16
+ * ============================================================================
+ * 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.
+ *
+ * •Extends: com.rho.studio.ui.core.base.BaseViewModel
+ * •Dependencies:
+ * •SessionManager: Singleton handling network/local session state.
+ * •Credentials: A data model encapsulating email and password logic.
+ *
+ * Core Logic Flows
+ * Real-time Validation
+ * •onEmailChanged() / onPasswordChanged():
+ * Triggered on every keystroke.
+ * •Updates the credentials model and
+ * immediately evaluates validation rules
+ * (blank checks, email regex, password length).
+ * Authentication Process
+ * •Trigger: onLoginClick() performs final validation and guards
+ * against concurrent attempts using the base loading state.
+ * •Execution: performLogin() utilizes launchWithLoading() to
+ * automatically manage the UI loading state and error trapping.
+ * •Network: Calls sessionManager.login().
+ * •Result Handling:
+ * •Success: Sets success toast and relies on SessionManager state.
+ * •Failure: Customizes error messages via the handleError() hook.
+ * Lifecycle & Architecture
+ * •Job Management:
+ * Relies on BaseViewModel's automated job tracking and cleanup
+ * to prevent memory leaks without manual cancellation logic.
+ * •State Reset:
+ * resetForm() provides a clean slate for the UI.
+ * ============================================================================
+ */
+package com.rho.studio.ui.features.auth
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.rho.studio.ui.core.base.BaseViewModel
+import com.rho.studio.ui.core.manager.SessionManager
+import com.rho.studio.ui.core.model.Credentials
+import kotlinx.coroutines.Job
+
+class LoginViewModel : BaseViewModel() {
+
+ // ==================== DEPENDENCIES ====================
+
+ private val sessionManager = SessionManager.getInstance()
+ private var loginJob: Job? = null
+
+ // ==================== FORM STATE ====================
+
+ 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) {
+ credentials.email = email
+ validateEmail()
+ validateForm()
+ }
+
+ fun onPasswordChanged(password: String) {
+ credentials.password = password
+ validatePassword()
+ validateForm()
+ }
+
+ private fun validateEmail() {
+ _emailError.value = when {
+ credentials.email.isBlank() -> "Email is required"
+ !credentials.isEmailValid -> "Please enter a valid email address"
+ else -> null
+ }
+ }
+
+ private fun validatePassword() {
+ _passwordError.value = when {
+ credentials.password.isBlank() -> "Password is required"
+ !credentials.isPasswordValid -> "Password must be at least 6 characters"
+ else -> null
+ }
+ }
+
+ private fun validateForm() {
+ _isFormValid.value = credentials.isValid
+ }
+
+ // ==================== ACTIONS ====================
+
+ fun onLoginClick() {
+ // Guard against multiple concurrent login attempts
+ if (isLoading.value == true) return
+
+ if (!credentials.isValid) {
+ validateEmail()
+ validatePassword()
+ showToast("Please fix the errors above")
+ return
+ }
+
+ performLogin()
+ }
+
+ private fun performLogin() {
+ loginJob = launchWithLoading(
+ block = {
+ // Delegate authentication to the session manager
+ sessionManager.login(credentials.email, credentials.password).join()
+
+ // Evaluate the authentication result
+ if (sessionManager.isAuthenticatedSync()) {
+ showToast("Login successful!")
+ clearError()
+ } else {
+ // Capture and handle the specific error from SessionManager
+ val errorMsg = sessionManager.error.value ?: "Authentication failed"
+ handleError(Exception(errorMsg))
+ }
+ },
+ onError = {
+ // General fallback if the login process crashes
+ showToast("Login process encountered an error. Please try again.")
+ }
+ )
+ }
+
+ // ==================== UTILITY METHODS ====================
+
+ fun resetForm() {
+ credentials.clear()
+ _emailError.value = null
+ _passwordError.value = null
+ _isFormValid.value = false
+ clearError()
+ clearToastMessage()
+ }
+
+ override fun handleError(e: Exception) {
+ // Ensure error messages are properly prefixed for context
+ val message = e.message ?: "An unknown error occurred"
+ val formattedMessage = message.takeIf { it.startsWith("Login failed") }
+ ?: "Login failed: $message"
+
+ super.handleError(Exception(formattedMessage, e.cause))
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/rho/studio/ui/features/auth/components/LoginButtonFragment.kt b/app/src/main/java/com/rho/studio/ui/features/auth/components/LoginButtonFragment.kt
new file mode 100644
index 0000000..78d2c95
--- /dev/null
+++ b/app/src/main/java/com/rho/studio/ui/features/auth/components/LoginButtonFragment.kt
@@ -0,0 +1,111 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ==============================================================================================
+ * 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/LoginEmailFragment.kt b/app/src/main/java/com/rho/studio/ui/features/auth/components/LoginEmailFragment.kt
new file mode 100644
index 0000000..6bbdf65
--- /dev/null
+++ b/app/src/main/java/com/rho/studio/ui/features/auth/components/LoginEmailFragment.kt
@@ -0,0 +1,92 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ==============================================================================================
+ * 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/LoginPasswordFragment.kt b/app/src/main/java/com/rho/studio/ui/features/auth/components/LoginPasswordFragment.kt
new file mode 100644
index 0000000..bb260d2
--- /dev/null
+++ b/app/src/main/java/com/rho/studio/ui/features/auth/components/LoginPasswordFragment.kt
@@ -0,0 +1,95 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ==============================================================================================
+ * 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/HeaderViewModel.kt b/app/src/main/java/com/rho/studio/ui/features/common/HeaderViewModel.kt
new file mode 100644
index 0000000..a935143
--- /dev/null
+++ b/app/src/main/java/com/rho/studio/ui/features/common/HeaderViewModel.kt
@@ -0,0 +1,41 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ==========================================================================
+ * File: HeaderViewModel.kt
+ * Author: Alexis Tercero
+ * Email: alexis.tercero@rho.studio
+ * Date: 2026-07-21
+ * ==========================================================================
+ * Description:
+ * ViewModel for the reusable PageHeaderFragment.
+ * Decouples the header from feature-specific ViewModels by sourcing
+ * user data directly from the SessionManager.
+ * ==========================================================================
+ */
+package com.rho.studio.ui.features.common
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.rho.studio.ui.core.base.BaseViewModel
+import com.rho.studio.ui.core.manager.SessionManager
+import com.rho.studio.ui.core.model.User
+
+class HeaderViewModel : BaseViewModel() {
+
+ private val sessionManager = SessionManager.getInstance()
+
+ val currentUser: LiveData = sessionManager.currentUser
+
+ private val _title = MutableLiveData()
+ val title: LiveData = _title
+
+ fun setTitle(newTitle: String) {
+ _title.value = newTitle
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/rho/studio/ui/data/model/Credentials.kt b/app/src/main/java/com/rho/studio/ui/features/common/PageFooterFragment.kt
similarity index 66%
rename from app/src/main/java/com/rho/studio/ui/data/model/Credentials.kt
rename to app/src/main/java/com/rho/studio/ui/features/common/PageFooterFragment.kt
index 19224c0..39f9970 100644
--- a/app/src/main/java/com/rho/studio/ui/data/model/Credentials.kt
+++ b/app/src/main/java/com/rho/studio/ui/features/common/PageFooterFragment.kt
@@ -1,4 +1,4 @@
-/*
+/**
* ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
* ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
* ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
@@ -7,18 +7,27 @@
* ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
*
* ==========================================================================
- * File: Credentials.kt
+ * File: MainActivity.kt
* Author: Alexis Tercero
* Email: alexis.tercero@rho.studio
- * Date: 2026-02-12
+ * Date: 2026-07-21
* ==========================================================================
* Description:
- * MVVM model,defining domain data.
+ * A reusable footer fragment for pages.
+ * Handles global actions like logout.
* ==========================================================================
*/
-package com.rho.studio.ui.data.model
+package com.rho.studio.ui.features.common
-data class Credentials(
- var email: String?,
- var password: String?
-)
+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/PageHeaderFragment.kt b/app/src/main/java/com/rho/studio/ui/features/common/PageHeaderFragment.kt
new file mode 100644
index 0000000..1d90406
--- /dev/null
+++ b/app/src/main/java/com/rho/studio/ui/features/common/PageHeaderFragment.kt
@@ -0,0 +1,55 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ==========================================================================
+ * 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
new file mode 100644
index 0000000..067ac72
--- /dev/null
+++ b/app/src/main/java/com/rho/studio/ui/features/home/HomeFragment.kt
@@ -0,0 +1,60 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ==============================================================================================
+ * 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/HomeViewModel.kt b/app/src/main/java/com/rho/studio/ui/features/home/HomeViewModel.kt
new file mode 100644
index 0000000..00e998f
--- /dev/null
+++ b/app/src/main/java/com/rho/studio/ui/features/home/HomeViewModel.kt
@@ -0,0 +1,66 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ==============================================================================================
+ * File: HomeViewModel.kt
+ * Author: Alexis Tercero
+ * Email: alexis.tercero@rho.studio
+ * Date: 2026-07-20
+ * ==============================================================================================
+ * Description: ViewModel for the Home feature, managing dashboard state and session data.
+ * ==============================================================================================
+ */
+package com.rho.studio.ui.features.home
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.rho.studio.ui.R
+import com.rho.studio.ui.core.base.BaseViewModel
+import com.rho.studio.ui.core.manager.SessionManager
+import com.rho.studio.ui.core.model.ServiceModule
+import com.rho.studio.ui.core.model.User
+
+class HomeViewModel : BaseViewModel() {
+
+ private val sessionManager = SessionManager.getInstance()
+
+ private val _currentUser = MutableLiveData(sessionManager.getCurrentUserSync())
+ val currentUser: LiveData = _currentUser
+
+ // Parametrized services for the dashboard
+ private val _services = MutableLiveData>(
+ listOf(
+ ServiceModule("inv", R.string.module_inventory, R.color.rho_red),
+ ServiceModule("sales", R.string.module_sales, R.color.rho_strong_gray),
+ ServiceModule("cust", R.string.module_customers, R.color.rho_strong_gray),
+ ServiceModule("rep", R.string.module_reports, R.color.rho_red)
+ )
+ )
+ val services: LiveData> = _services
+
+ private val _navigateToService = MutableLiveData()
+ val navigateToService: LiveData = _navigateToService
+
+ fun onServiceClick(serviceId: String) {
+ _navigateToService.value = serviceId
+ }
+
+ fun onServiceNavigated() {
+ _navigateToService.value = null
+ }
+
+ /**
+ * Trigger user logout via SessionManager.
+ * The transition to the login screen is handled by the MainActivity observer.
+ */
+ fun logout() {
+ launchWithLoading({
+ sessionManager.logout().join()
+ })
+ }
+}
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
new file mode 100644
index 0000000..b9f7cfe
--- /dev/null
+++ b/app/src/main/java/com/rho/studio/ui/features/home/ServiceAdapter.kt
@@ -0,0 +1,64 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ==========================================================================
+ * 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
new file mode 100644
index 0000000..39b37f5
--- /dev/null
+++ b/app/src/main/java/com/rho/studio/ui/features/home/components/HomeServicesFragment.kt
@@ -0,0 +1,63 @@
+/**
+ * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
+ * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
+ * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
+ * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
+ * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
+ *
+ * ==========================================================================
+ * 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/viewmodel/LoginViewModel.kt b/app/src/main/java/com/rho/studio/ui/viewmodel/LoginViewModel.kt
deleted file mode 100644
index 55630b7..0000000
--- a/app/src/main/java/com/rho/studio/ui/viewmodel/LoginViewModel.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
- * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
- * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
- * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
- * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
- * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
- *
- * ==========================================================================
- * File: LoginViewModel.kt
- * Author: Alexis Tercero
- * Email: alexis.tercero@rho.studio
- * Date: 2026-02-12
- * ==========================================================================
- * Description:
- * MVVM LoginViewModel for a login logic.
- * ==========================================================================
- */
-package com.rho.studio.ui.viewmodel
-
-import android.util.Patterns
-import androidx.databinding.BaseObservable
-import androidx.databinding.Bindable
-import com.rho.studio.ui.BR
-import com.rho.studio.ui.data.model.Credentials
-
-class LoginViewModel : BaseObservable() {
- private val credentialslive: Credentials = Credentials("", "")
-
- private val successMessage = "Login successful"
- private val errorMessage = "Email or Password is not valid"
-
- @get:Bindable
- var toastMessage: String? = null
- private set(value) {
- field = value
- notifyPropertyChanged(BR.toastMessage)
- }
-
- // Getter and setter methods for email variable
- @get:Bindable
- var userEmail: String?
- get() = credentialslive.email // 1. Reads from the Credentials object
- set(value) {
- credentialslive.email = value // 2. Updates the Credentials object
- notifyPropertyChanged(BR.userEmail) // 3. Tells the UI to refresh
- }
-
- // Getter and setter methods for password variable
- @get:Bindable
- var userPassword: String?
- get() = credentialslive.password
- set(value) {
- credentialslive.password = value
- notifyPropertyChanged(BR.userPassword)
- }
-
- // Actions to be performed when the user clicks the LOGIN button
- fun onButtonClicked() {
- toastMessage = if (isValid()) successMessage else errorMessage
- }
- // Method to ensure fields are not empty and email/password validation
- private fun isValid(): Boolean {
- return !userEmail.isNullOrEmpty() && Patterns.EMAIL_ADDRESS.matcher(userEmail!!).matches()
- && !userPassword.isNullOrEmpty() && userPassword!!.length > 5
- }
-}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/bg_login_gradient.xml b/app/src/main/res/drawable/bg_login_gradient.xml
new file mode 100644
index 0000000..9f482d7
--- /dev/null
+++ b/app/src/main/res/drawable/bg_login_gradient.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_email.xml b/app/src/main/res/drawable/ic_email.xml
new file mode 100644
index 0000000..727882e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_email.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_lock.xml b/app/src/main/res/drawable/ic_lock.xml
new file mode 100644
index 0000000..babd47d
--- /dev/null
+++ b/app/src/main/res/drawable/ic_lock.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 31fb2ed..4fcf6d0 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -24,69 +24,35 @@
+ name="sessionManager"
+ type="com.rho.studio.ui.core.manager.SessionManager" />
-
+ android:layout_height="match_parent">
-
-
-
-
-
-
-
+ android:layout_height="match_parent"
+ app:defaultNavHost="true"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
-
-
+ android:visibility="gone"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
-
+
\ 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
new file mode 100644
index 0000000..a3faac7
--- /dev/null
+++ b/app/src/main/res/layout/fragment_home.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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
new file mode 100644
index 0000000..cabed5e
--- /dev/null
+++ b/app/src/main/res/layout/fragment_home_services.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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
new file mode 100644
index 0000000..bc775bc
--- /dev/null
+++ b/app/src/main/res/layout/fragment_login.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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
new file mode 100644
index 0000000..02ba0d2
--- /dev/null
+++ b/app/src/main/res/layout/fragment_login_button.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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
new file mode 100644
index 0000000..22112a8
--- /dev/null
+++ b/app/src/main/res/layout/fragment_login_email.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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
new file mode 100644
index 0000000..9d67e6f
--- /dev/null
+++ b/app/src/main/res/layout/fragment_login_password.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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
new file mode 100644
index 0000000..535ed35
--- /dev/null
+++ b/app/src/main/res/layout/fragment_page_footer.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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
new file mode 100644
index 0000000..62c9643
--- /dev/null
+++ b/app/src/main/res/layout/fragment_page_header.xml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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
new file mode 100644
index 0000000..e5a9813
--- /dev/null
+++ b/app/src/main/res/layout/item_service.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml
new file mode 100644
index 0000000..11b92f9
--- /dev/null
+++ b/app/src/main/res/navigation/nav_graph.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 7502eb9..f767871 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -10,4 +10,9 @@
#FF018786
#FF000000
#FFFFFFFF
+ #D32F2F
+ #333333
+ #FF0000
+ #4CAF50
+ #C0C0C0
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index cfba2d0..d118a9e 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,7 +1,22 @@
- UI
- Login
Enter your password
Enter your Email ID
Rho.Studio app
+ Hello blank fragment
+ RhoStudio UI
+ Login
+ Email address
+ Password
+ Email is required
+ Please enter a valid email address
+ Password is required
+ Password must be at least 6 characters
+ Login successful! Welcome back.
+ Welcome, %s!
+ Logout
+ Home Dashboard
+ Inventory
+ Sales
+ Customers
+ Reports
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 8246950..7d9df29 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -2,4 +2,15 @@
+
+
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index 952b930..895ed18 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,6 +1,5 @@
-// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
- alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
+ alias(libs.plugins.kotlin.parcelize) apply false
}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 20e2a01..410c934 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -15,9 +15,17 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
+android.enableJetifier=false
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
-android.nonTransitiveRClass=true
\ No newline at end of file
+android.nonTransitiveRClass=true
+android.defaults.buildfeatures.resvalues=false
+android.sdk.defaultTargetSdkToCompileSdkIfUnset=true
+android.usesSdkInManifest.disallowed=true
+android.uniquePackageNames=false
+android.dependency.useConstraints=true
+android.r8.strictFullModeForKeepRules=false
+android.r8.optimizedResourceShrinking=true
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 5d464bd..101a1d6 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,16 +1,25 @@
[versions]
-agp = "8.12.3"
-kotlin = "2.0.21"
-coreKtx = "1.17.0"
+agp = "9.2.1"
+compiler = "3.1.4"
+fragmentKtx = "1.8.9"
+gson = "2.14.0"
+kotlin = "2.4.10"
+kotlinParcelize = "2.4.10"
+coreKtx = "1.19.0"
junit = "4.13.2"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
-lifecycleRuntimeKtx = "2.9.4"
-activityCompose = "1.11.0"
-composeBom = "2024.09.00"
+lifecycleRuntimeKtx = "2.11.0"
+activityCompose = "1.13.0"
+composeBom = "2026.06.01"
+material = "1.14.0"
+navigation = "2.9.8"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragmentKtx" }
+compiler = { module = "com.android.databinding:compiler", version.ref = "compiler" }
+gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
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" }
@@ -24,9 +33,12 @@ 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-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" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+legacy-kapt = { id = "com.android.legacy-kapt", version.ref = "agp" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
-
+kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlinParcelize" }
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e708b1c
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index b5a15e8..a697e25 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Wed Nov 19 15:11:45 CST 2025
+#Wed Jul 15 18:16:29 CST 2026
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 08b2e61..914b2d2 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -11,6 +11,9 @@ pluginManagement {
gradlePluginPortal()
}
}
+plugins {
+ id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
+}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {