From ec1ae3c297a213cced2741872d4a8fd0d9414192 Mon Sep 17 00:00:00 2001 From: Alexis Tercero Date: Wed, 29 Jul 2026 18:03:46 -0600 Subject: [PATCH 1/5] MIGRATION | #18 #20 login feature from Fragments to Jetpack Compose - Remove `BaseFragment`, `LoginFragment`, and sub-component fragments (`LoginEmailFragment`, `LoginPasswordFragment`, `LoginButtonFragment`) along with their associated XML DataBinding layouts. - Implement `LoginScreen` and modular UI components (`LoginEmailField`, `LoginPasswordField`, `LoginButton`) using Jetpack Compose and Material3. - Refactor `LoginViewModel` to use Compose `mutableStateOf` for `email` and `password` tracking. - Centralize input validation debouncing logic (300ms) within the `LoginViewModel` using `viewModelScope` to replace fragment-level handling. - Update `LoginViewModel` to handle pure state-driven UI updates, ensuring unidirectional data flow between the ViewModel and Composables. Signed-off-by: Alexis Tercero --- .../rho/studio/ui/core/base/BaseFragment.kt | 220 ------------------ .../studio/ui/features/auth/LoginFragment.kt | 91 -------- .../studio/ui/features/auth/LoginScreen.kt | 101 ++++++++ .../studio/ui/features/auth/LoginViewModel.kt | 70 +++--- .../features/auth/components/LoginButton.kt | 57 +++++ .../auth/components/LoginButtonFragment.kt | 111 --------- .../auth/components/LoginEmailField.kt | 82 +++++++ .../auth/components/LoginEmailFragment.kt | 92 -------- .../auth/components/LoginPasswordField.kt | 107 +++++++++ .../auth/components/LoginPasswordFragment.kt | 95 -------- app/src/main/res/layout/fragment_login.xml | 71 ------ .../main/res/layout/fragment_login_button.xml | 52 ----- .../main/res/layout/fragment_login_email.xml | 50 ---- .../res/layout/fragment_login_password.xml | 50 ---- 14 files changed, 391 insertions(+), 858 deletions(-) delete mode 100644 app/src/main/java/com/rho/studio/ui/core/base/BaseFragment.kt delete mode 100644 app/src/main/java/com/rho/studio/ui/features/auth/LoginFragment.kt create mode 100644 app/src/main/java/com/rho/studio/ui/features/auth/LoginScreen.kt create mode 100644 app/src/main/java/com/rho/studio/ui/features/auth/components/LoginButton.kt delete mode 100644 app/src/main/java/com/rho/studio/ui/features/auth/components/LoginButtonFragment.kt create mode 100644 app/src/main/java/com/rho/studio/ui/features/auth/components/LoginEmailField.kt delete mode 100644 app/src/main/java/com/rho/studio/ui/features/auth/components/LoginEmailFragment.kt create mode 100644 app/src/main/java/com/rho/studio/ui/features/auth/components/LoginPasswordField.kt delete mode 100644 app/src/main/java/com/rho/studio/ui/features/auth/components/LoginPasswordFragment.kt delete mode 100644 app/src/main/res/layout/fragment_login.xml delete mode 100644 app/src/main/res/layout/fragment_login_button.xml delete mode 100644 app/src/main/res/layout/fragment_login_email.xml delete mode 100644 app/src/main/res/layout/fragment_login_password.xml diff --git a/app/src/main/java/com/rho/studio/ui/core/base/BaseFragment.kt b/app/src/main/java/com/rho/studio/ui/core/base/BaseFragment.kt deleted file mode 100644 index 18aaa72..0000000 --- a/app/src/main/java/com/rho/studio/ui/core/base/BaseFragment.kt +++ /dev/null @@ -1,220 +0,0 @@ -/** - * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗ - * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗ - * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║ - * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║ - * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝ - * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ - * - * ============================================================================================== - * File: BaseFragment.kt - * Author: Alexis Tercero - * Email: alexis.tercero@rho.studio - * Date: 2026-07-15 - * ============================================================================================== - * Description: - * A generic base class for [Fragment]s that utilize DataBinding and BaseViewModel. - * ============================================================================================== - */ -package com.rho.studio.ui.core.base - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Toast -import androidx.annotation.LayoutRes -import androidx.databinding.DataBindingUtil -import androidx.databinding.ViewDataBinding -import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModel -import com.rho.studio.ui.core.manager.SessionManager - -/** - * BaseFragment - Base class for all feature fragments - * - * A generic base class for [Fragment]s that utilize DataBinding and [BaseViewModel]. - * - * This base class standardizes the fragment lifecycle, enforces memory leak prevention - * for view bindings, and provides common hooks for initialization and observation. - * - * ### Key Features: - * 1. **Automated DataBinding:** Inflates the layout and attaches the ViewModel automatically. - * 2. **Lifecycle Safety:** Manages the backing property for [_binding] to prevent - * memory leaks by nulling it out in [onDestroyView]. - * 3. **Session Integration:** Provides lazy access to the global [SessionManager]. - * 4. **Standardized Workflow:** Defines a clear execution order: Binding -> [initializeViews] -> [setupObservers]. - * - * ### How to implement: - * ```kotlin - * class LoginFragment : BaseFragment() { - * override val viewModel: LoginViewModel by viewModels() - * override val layoutId: Int = R.layout.fragment_login - * override val bindingVariable: Int = BR.viewModel - * - * override fun initializeViews() { - * binding.loginButton.setOnClickListener { ... } - * } - * } - * ``` - * - * @param T The specific [ViewDataBinding] class generated for the fragment's layout. - * @param VM The [ViewModel] class associated with this fragment. - */ -abstract class BaseFragment : Fragment() { - - // ==================== ABSTRACT PROPERTIES ==================== - - /**It declares a read-only property named viewModel of type VM - * (the specific ViewModel type provided when the subclass is created).*/ - protected abstract val viewModel: VM - - @get:LayoutRes - protected abstract val layoutId: Int - - protected abstract val bindingVariable: Int - - // ==================== BINDING WITH MEMORY LEAK PROTECTION ==================== - - private var _binding: T? = null - - /** - * Protected binding property - safe access only between onCreateView and onDestroyView - * Throws IllegalStateException if accessed outside this window - */ - protected val binding: T - get() = _binding ?: throw IllegalStateException( - "Cannot access binding after onDestroyView or before onCreateView" - ) - - // ==================== OPTIONAL DEPENDENCIES ==================== - - /** - * SessionManager - lazy initialized, only created if accessed - * Made open so fragments can override if needed - */ - protected open val sessionManager: SessionManager by lazy { - SessionManager.getInstance() - } - - // ==================== LIFECYCLE METHODS ==================== - - final override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - //backing property - _binding = DataBindingUtil.inflate(inflater, layoutId, container, false) - - /** snippet is used in - * Android Data Binding to connect your layout views to a data source - * (usually a ViewModel) and ensure the UI reflects changes immediately. - * */ - with(binding) { - /**layout uses LiveData, the binding needs a lifecycle owner to observe that data. - * Without this line, LiveData changes in your ViewModel - * will not automatically update the UI*/ - lifecycleOwner = viewLifecycleOwner - setVariable(bindingVariable, viewModel) - executePendingBindings() // Immediate UI update - } - - return binding.root - } - - /** - * Set up views and observers in this method - */ - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setupCommonObservers() - initializeViews() - setupObservers() - } - - /** - * Critical for memory leak prevention - */ - final override fun onDestroyView() { - cleanupBinding() - super.onDestroyView() - _binding = null - } - - /** - * Sets up automatic observation of common ViewModel LiveData. - * This eliminates boilerplate in child fragments. - */ - private fun setupCommonObservers() { - // Loading state - override onLoadingStateChanged for custom UI - viewModel.isLoading.observe(viewLifecycleOwner) { isLoading -> - onLoadingStateChanged(isLoading) - } - - // Error messages - automatically shown and cleared - viewModel.error.observe(viewLifecycleOwner) { error -> - error?.let { - onError(it) - viewModel.clearError() - } - } - - // Toast messages - automatically shown and cleared - viewModel.toastMessage.observe(viewLifecycleOwner) { message -> - message?.let { - onToastMessage(it) - viewModel.clearToastMessage() - } - } - } - - // ==================== EXTENSION POINTS FOR CHILD FRAGMENTS ==================== - - /** - * Called after binding is set up - use for view initialization - * Examples: setting up RecyclerView, adapters, click listeners - */ - protected open fun initializeViews() {} - - /** - * Called after initializeViews - use for LiveData observers - * Separated from initializeViews for better organization - */ - protected open fun setupObservers() {} - - /** - * Optional cleanup method for fragments that need to release resources - * Called before binding is nulled - */ - protected open fun cleanupBinding() { - // Override in child fragments if needed - } - - // ==================== UTILITY METHODS ==================== - - /** Called when loading state changes - override for custom loading UI */ - protected open fun onLoadingStateChanged(isLoading: Boolean) {} - - /** Called when an error occurs - override for custom error handling */ - protected open fun onError(message: String) { - Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show() - } - - /** Called for toast messages - override for custom toast behavior */ - protected open fun onToastMessage(message: String) { - Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() - } - - /** - * Check if binding is available (between onCreateView and onDestroyView) - */ - protected fun isBindingAvailable(): Boolean = _binding != null - - /** - * Safely execute code that requires binding - */ - protected fun withBinding(block: (T) -> Unit) { - _binding?.let(block) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/rho/studio/ui/features/auth/LoginFragment.kt b/app/src/main/java/com/rho/studio/ui/features/auth/LoginFragment.kt deleted file mode 100644 index c0b37b6..0000000 --- a/app/src/main/java/com/rho/studio/ui/features/auth/LoginFragment.kt +++ /dev/null @@ -1,91 +0,0 @@ -/** - * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗ - * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗ - * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║ - * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║ - * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝ - * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ - * - * ============================================================================================== - * File: LoginFragment.kt - * Author: Alexis Tercero - * Email: alexis.tercero@rho.studio - * Date: 2026-07-16 - * ============================================================================================== - * LoginFragment serves as the primary orchestration layer for the authentication feature. - * It acts as a parent container that manages the lifecycle, state observation, - * and composition of specialized login components. It inherits from BaseFragment - * to leverage standardized ViewBinding and ViewModel integration. - * ============================================================================================== - */ -package com.rho.studio.ui.features.auth - -import android.view.View -import androidx.fragment.app.viewModels -import com.rho.studio.ui.BR -import com.rho.studio.ui.R -import com.rho.studio.ui.core.base.BaseFragment -import com.rho.studio.ui.databinding.FragmentLoginBinding -import com.rho.studio.ui.features.auth.components.LoginButtonFragment -import com.rho.studio.ui.features.auth.components.LoginEmailFragment -import com.rho.studio.ui.features.auth.components.LoginPasswordFragment - - -/** - * LoginFragment - Main container for authentication - * - * Feature: auth - * - * Responsibilities: - * • Reusability: Individual login components can be reused in other flows - * (e.g., Registration). - * • Separation of Concerns: LoginFragment manages the "How" (layout/navigation), - * while child fragments handle the "What" (specific inputs). - * - * 1. Fragment Composition - * The fragment initializes the UI by embedding three core sub-components - * into designated containers within fragment_login.xml: - * •LoginEmailFragment: Handles email input and validation. - * •LoginPasswordFragment: Handles password input and visibility. - * •LoginButtonFragment: Handles the submission trigger. - * 2. State Observation - * It observes the LoginViewModel to react to the following states: - * •Toast Messages: Short-lived UI feedback (e.g., "Welcome back"). - * •Errors: Long-lived feedback for failed authentication attempts. - * •Loading State: Toggles the visibility of a global ProgressBar - * to block interaction during network requests. - * 3. Lifecycle Management - * •Initialization: - * Uses childFragmentManager to transactionally inject components - * once the fragment is attached. - * •Cleanup: - * Ensures that transient UI states (like error messages or toasts) - * are cleared from the ViewModel when the view is destroyed - * to prevent stale data on return. - */ -class LoginFragment : BaseFragment() { - - override val viewModel: LoginViewModel by viewModels() - override val layoutId: Int = R.layout.fragment_login - override val bindingVariable: Int = BR.viewModel - - override fun initializeViews() { - if (isAdded) { - setupChildFragments() - } - } - - override fun cleanupBinding() { - // Enforce clean slate policy defined in feature docs - viewModel.clearToastMessage() - viewModel.clearError() - } - private fun setupChildFragments() { - childFragmentManager.beginTransaction().apply { - replace(R.id.email_container, LoginEmailFragment()) - replace(R.id.password_container, LoginPasswordFragment()) - replace(R.id.button_container, LoginButtonFragment()) - commitAllowingStateLoss() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/rho/studio/ui/features/auth/LoginScreen.kt b/app/src/main/java/com/rho/studio/ui/features/auth/LoginScreen.kt new file mode 100644 index 0000000..b5a43bf --- /dev/null +++ b/app/src/main/java/com/rho/studio/ui/features/auth/LoginScreen.kt @@ -0,0 +1,101 @@ +/** + * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗ + * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗ + * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝ + * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ + * + * ============================================================================ + * File: LoginScreen.kt + * Author: Alexis Tercero + * Email: alexis.tercero@rho.studio + * Date: 2026-07-29 + * ============================================================================ + * Description: Implementation of the Login screen using Jetpack Compose and + * MVVM architecture. + * + * Features: + * - State Management: Utilizes LiveData observed as Compose State for reactive + * UI updates (e.g., loading states, input validation). + * - Unidirectional Data Flow (UDF): Events are passed from the UI to the + * ViewModel, while State flows down from the ViewModel to the Composables. + * - Component Modularization: Extracts input fields and buttons into dedicated + * sub-components for reusability and cleaner code structure. + * - Theming: Integrates custom branding colors (RhoRed, SilverGray) via + * gradient backgrounds and Material3 typography. + * ============================================================================ + */ +package com.rho.studio.ui.features.auth + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.rho.studio.ui.R +import com.rho.studio.ui.features.auth.components.LoginButton +import com.rho.studio.ui.features.auth.components.LoginEmailField +import com.rho.studio.ui.features.auth.components.LoginPasswordField +import com.rho.studio.ui.ui.theme.Black +import com.rho.studio.ui.ui.theme.RhoRed +import com.rho.studio.ui.ui.theme.RhoStrongGray +import com.rho.studio.ui.ui.theme.SilverGray +import com.rho.studio.ui.ui.theme.White + +@Composable +fun LoginScreen( + viewModel: LoginViewModel, + modifier: Modifier = Modifier +) { + val isLoading by viewModel.isLoading.observeAsState(false) + + Box( + modifier = modifier + .fillMaxSize() + .background( + brush = Brush.verticalGradient( + colors = listOf( + RhoRed, + SilverGray, + Black + ) + ) + ) + .padding(24.dp), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(R.string.app_name), + fontSize = 32.sp, + fontWeight = FontWeight.Bold, + color = Color.Black + ) + Spacer(modifier = Modifier.height(48.dp)) + LoginEmailField(viewModel = viewModel) + Spacer(modifier = Modifier.height(16.dp)) + LoginPasswordField(viewModel = viewModel) + Spacer(modifier = Modifier.height(24.dp)) + LoginButton(viewModel = viewModel) + } + } +} diff --git a/app/src/main/java/com/rho/studio/ui/features/auth/LoginViewModel.kt b/app/src/main/java/com/rho/studio/ui/features/auth/LoginViewModel.kt index 52e3434..cfa794b 100644 --- a/app/src/main/java/com/rho/studio/ui/features/auth/LoginViewModel.kt +++ b/app/src/main/java/com/rho/studio/ui/features/auth/LoginViewModel.kt @@ -10,13 +10,14 @@ * File: LoginViewModel.kt * Author: Alexis Tercero * Email: alexis.tercero@rho.studio - * Date: 2026-07-16 + * Date: 2026-07-29 * ============================================================================ * Description: * The LoginViewModel manages the state and business logic for the - * Authentication screen, leveraging the Rho Studio BaseViewModel architecture. - * It handles user input validation, manages asynchronous login requests - * via SessionManager, and exposes reactive UI states using LiveData. + * Authentication screen in a pure Jetpack Compose environment. + * It leverages the Rho Studio BaseViewModel architecture to handle + * user input validation, asynchronous login requests via SessionManager, + * and reactive UI states. * * •Extends: com.rho.studio.ui.core.base.BaseViewModel * •Dependencies: @@ -24,12 +25,14 @@ * •Credentials: A data model encapsulating email and password logic. * * Core Logic Flows - * Real-time Validation + * State-Driven Input + * •email / password: Uses Compose `mutableStateOf` to provide + * immediate, observable reactivity for the UI layer. + * Real-time & Debounced Validation * •onEmailChanged() / onPasswordChanged(): - * Triggered on every keystroke. - * •Updates the credentials model and - * immediately evaluates validation rules - * (blank checks, email regex, password length). + * Triggered on every keystroke, updating the state immediately. + * •300ms Debounce: Logic moved from Fragments to the ViewModel, + * ensuring validation is only performed after the user pauses typing. * Authentication Process * •Trigger: onLoginClick() performs final validation and guards * against concurrent attempts using the base loading state. @@ -44,52 +47,67 @@ * Relies on BaseViewModel's automated job tracking and cleanup * to prevent memory leaks without manual cancellation logic. * •State Reset: - * resetForm() provides a clean slate for the UI. + * resetForm() provides a clean, secure slate for the UI by + * clearing Compose states and the underlying model. * ============================================================================ */ package com.rho.studio.ui.features.auth +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope import com.rho.studio.ui.core.base.BaseViewModel import com.rho.studio.ui.core.manager.SessionManager import com.rho.studio.ui.core.model.Credentials import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch class LoginViewModel : BaseViewModel() { - // ==================== DEPENDENCIES ==================== - private val sessionManager = SessionManager.getInstance() private var loginJob: Job? = null - + private var emailDebounceJob: Job? = null + private var passwordDebounceJob: Job? = null // ==================== FORM STATE ==================== - + var email by mutableStateOf("") + private set + var password by mutableStateOf("") + private set val credentials = Credentials() - // ==================== UI STATE ==================== - private val _emailError = MutableLiveData() val emailError: LiveData = _emailError - private val _passwordError = MutableLiveData() val passwordError: LiveData = _passwordError - private val _isFormValid = MutableLiveData(false) val isFormValid: LiveData = _isFormValid - // ==================== FORM VALIDATION ==================== - fun onEmailChanged(email: String) { + this.email = email credentials.email = email - validateEmail() - validateForm() + + emailDebounceJob?.cancel() + emailDebounceJob = viewModelScope.launch { + delay(300) + validateEmail() + validateForm() + } } fun onPasswordChanged(password: String) { + this.password = password credentials.password = password - validatePassword() - validateForm() + + passwordDebounceJob?.cancel() + passwordDebounceJob = viewModelScope.launch { + delay(300) + validatePassword() + validateForm() + } } private fun validateEmail() { @@ -113,7 +131,6 @@ class LoginViewModel : BaseViewModel() { } // ==================== ACTIONS ==================== - fun onLoginClick() { // Guard against multiple concurrent login attempts if (isLoading.value == true) return @@ -152,8 +169,9 @@ class LoginViewModel : BaseViewModel() { } // ==================== UTILITY METHODS ==================== - fun resetForm() { + email = "" + password = "" credentials.clear() _emailError.value = null _passwordError.value = null diff --git a/app/src/main/java/com/rho/studio/ui/features/auth/components/LoginButton.kt b/app/src/main/java/com/rho/studio/ui/features/auth/components/LoginButton.kt new file mode 100644 index 0000000..c715b8a --- /dev/null +++ b/app/src/main/java/com/rho/studio/ui/features/auth/components/LoginButton.kt @@ -0,0 +1,57 @@ +/** + * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗ + * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗ + * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║ + * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝ + * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ + * + * ============================================================================ + * File: LoginButton.kt (composable UI) + * Author: Alexis Tercero + * Email: alexis.tercero@rho.studio + * Date: 2026-07-29 + * ============================================================================ + * Description: A custom Jetpack Compose button component for the Login screen. + * It observes the LoginViewModel state to handle validation logic + * and loading states, automatically disabling interaction and + * updating its UI when a login attempt is in progress. + * ============================================================================ + */ +package com.rho.studio.ui.features.auth.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.rho.studio.ui.R +import com.rho.studio.ui.features.auth.LoginViewModel +import com.rho.studio.ui.ui.theme.RhoRed + +@Composable +fun LoginButton( + viewModel: LoginViewModel, + modifier: Modifier = Modifier +) { + val isFormValid by viewModel.isFormValid.observeAsState(false) + val isLoading by viewModel.isLoading.observeAsState(false) + + Button( + onClick = { viewModel.onLoginClick() }, + modifier = modifier + .fillMaxWidth() + .height(56.dp), + enabled = isFormValid && !isLoading, + colors = ButtonDefaults.buttonColors(containerColor = RhoRed) + ) { + Text(text = stringResource(if (isLoading) R.string.rho_studio_app else R.string.login)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/rho/studio/ui/features/auth/components/LoginButtonFragment.kt b/app/src/main/java/com/rho/studio/ui/features/auth/components/LoginButtonFragment.kt deleted file mode 100644 index 78d2c95..0000000 --- a/app/src/main/java/com/rho/studio/ui/features/auth/components/LoginButtonFragment.kt +++ /dev/null @@ -1,111 +0,0 @@ -/** - * ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗ - * ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗ - * ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║ - * ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║ - * ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝ - * ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ - * - * ============================================================================================== - * File: LoginButtonFragment.kt - * Author: Alexis Tercero - * Email: alexis.tercero@rho.studio - * Date: 2026-07-16 - * ============================================================================================== - * Description: Manages the primary action button and loading - * state for the login flow. - * ============================================================================================== - */ -package com.rho.studio.ui.features.auth.components - -import android.os.Bundle -import android.view.View -import android.widget.Toast -import androidx.databinding.BindingAdapter -import androidx.fragment.app.viewModels -import androidx.lifecycle.lifecycleScope -import com.rho.studio.ui.BR -import com.rho.studio.ui.R -import com.rho.studio.ui.core.base.BaseFragment -import com.rho.studio.ui.databinding.FragmentLoginButtonBinding -import com.rho.studio.ui.features.auth.LoginViewModel -import kotlinx.coroutines.launch - -/** - * LoginEmailFragment - Reusable email input component - * - * Feature: auth - * Purpose: Manages the primary action button and loading - * state for the login flow. - * - * This fragment acts as a reactive component within the Auth module. It synchronizes - * the button's enabled state with form validation and displays progress indicators - * during asynchronous login operations. - * - * ### Key Behaviors: - * 1. **Shared State:** Scoped to the parent fragment via [requireParentFragment] to - * interact with the shared [LoginViewModel]. - * 2. **Reactive UI:** Automatically enables/disables the login button based on - * [LoginViewModel.isFormValid] and [LoginViewModel.isLoading]. - * 3. **Visual Feedback:** Manages the visibility of a progress bar during the - * authentication network simulation. - * 4. **Binding Adapters:** Provides a static [showToast] adapter to allow the XML - * layout to reactively trigger system toasts based on ViewModel messages. - * - * ### Usage in XML: - * ```xml - *