Skip to main content

Módulo de Login Reutilizable - Proyecto Demo Alpura

Este módulo implementa el flujo completo de autenticación de usuario usando Compose, Hilt, ViewModel, Retrofit, DataStore y Tink, cumpliendo con los lineamientos definidos en el estándar de arquitectura mobile de Alpura.

🎯 Alcance del Módulo

  • Pantalla de login funcional usando Jetpack Compose + Material3
  • ViewModel desacoplado que usa un UseCase del dominio
  • Comunicación con un API REST externa con autenticación mediante Bearer Token
  • Persistencia segura de token usando DataStore + Tink
  • Pruebas unitarias y de UI
  • Documentación completa con KDoc
  • Modularización para reutilización en otros proyectos

🗂️ Estructura del Módulo

features/
└── login/
├── ui/screens/ # LoginScreen.kt
├── ui/state/ # LoginUiState.kt
├── presentation/ # LoginViewModel.kt
├── domain/model/ # AuthToken.kt, Credentials.kt
├── domain/usecase/ # LoginUseCase.kt
├── domain/repository/ # AuthRepository.kt (interface)
├── data/repository/ # AuthRepositoryImpl.kt
├── data/datasource/remote/ # AuthApiService.kt (Retrofit)
├── di/ # LoginModule.kt (Hilt bindings)
└── test/ # Unit tests (MockK)

features/login/
├── ui/screens/LoginScreen.kt # Pantalla Compose con campos y botón de login
├── presentation/LoginViewModel.kt # ViewModel que expone y gestiona el estado
├── domain/model/Credentials.kt # Modelo de credenciales
├── domain/usecase/DoLoginUseCase.kt # Caso de uso principal
├── domain/repository/AuthRepository.kt # Interface de repositorio
├── data/repository/AuthRepositoryImpl.kt # Implementación del repositorio
├── data/datasource/remote/AuthApi.kt # API Retrofit para login
├── di/LoginModule.kt # Configuración de Hilt

🧱 Flujo de Autenticación


💡 API Utilizada

Usaremos un servicio demo para autenticación, por ejemplo:

POST https://reqres.in/api/login
{
"email": "eve.holt@reqres.in",
"password": "cityslicka"
}

Respuesta esperada:

{ "token": "QpwL5tke4Pnpja7X4" }

🔐 Almacenamiento Seguro con DataStore + Tink

Almacenaremos el token usando DataStore y cifrado con Tink (ver sección 3.15).

@Provides
@Singleton
fun provideEncryptedPrefs(
context: Context,
tinkKey: Aead
): DataStore<Preferences> = EncryptedDataStoreFactory.create(
produceFile = { context.dataStoreFile("secure_prefs.preferences_pb") },
encryption = EncryptedDataStoreEncryption(tinkKey),
)

🔒 Toda la configuración debe seguir el estándar definido en la sección 3.15 del documento técnico.


🧪 Pruebas

  • Pruebas unitarias para LoginUseCase, LoginViewModel, AuthRepositoryImpl
  • Pruebas de UI con Compose Test para LoginScreen

🔄 Modularización

El módulo de login será publicable como un artefacto Gradle para reutilizar en otros proyectos:

implementation("com.alpura.mobile:login:{version}")

La vista (pantalla y tema) podrá ser personalizada sin romper la lógica reutilizable.


🖥️ UI: LoginScreen.kt

@Composable
fun LoginScreen(
onLoginSuccess: () -> Unit,
viewModel: LoginViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsState()

Column(modifier = Modifier.padding(16.dp)) {
OutlinedTextField(
value = uiState.email,
onValueChange = { viewModel.onEmailChange(it) },
label = { Text("Email") }
)
OutlinedTextField(
value = uiState.password,
onValueChange = { viewModel.onPasswordChange(it) },
label = { Text("Password") },
visualTransformation = PasswordVisualTransformation()
)
Button(onClick = { viewModel.onLogin() }) {
Text("Iniciar sesión")
}

if (uiState.success) {
LaunchedEffect(true) { onLoginSuccess() }
}
}
}

🧠 Esta pantalla consume directamente el LoginViewModel y reacciona a su estado usando StateFlow.


🧠 ViewModel: LoginViewModel.kt

@HiltViewModel
class LoginViewModel @Inject constructor(
private val doLoginUseCase: DoLoginUseCase
) : ViewModel() {

var uiState by mutableStateOf(LoginUiState())
private set

fun onEmailChange(value: String) {
uiState = uiState.copy(email = value)
}

fun onPasswordChange(value: String) {
uiState = uiState.copy(password = value)
}

fun onLogin() {
viewModelScope.launch {
uiState = uiState.copy(loading = true)
val result = doLoginUseCase(uiState.email, uiState.password)
uiState = uiState.copy(success = result, loading = false)
}
}
}

📦 El estado de la pantalla se mantiene inmutable con copy() para cada cambio.


🧪 Caso de Uso: DoLoginUseCase.kt

class DoLoginUseCase @Inject constructor(
private val repository: AuthRepository
) {
suspend operator fun invoke(email: String, password: String): Boolean {
return repository.login(Credentials(email, password))
}
}

📡 API: AuthApi.kt

interface AuthApi {
@POST("login")
suspend fun login(@Body credentials: Credentials): AuthResponse
}

🗃️ Repositorio: AuthRepository.kt + AuthRepositoryImpl.kt

interface AuthRepository {
suspend fun login(credentials: Credentials): Boolean
}
class AuthRepositoryImpl @Inject constructor(
private val api: AuthApi
) : AuthRepository {
override suspend fun login(credentials: Credentials): Boolean {
return try {
val response = api.login(credentials)
response.token.isNotEmpty()
} catch (e: Exception) {
false
}
}
}

⚙️ Inyección con Hilt: LoginModule.kt

@Module
@InstallIn(SingletonComponent::class)
object LoginModule {

@Provides
fun provideAuthApi(): AuthApi = RetrofitClient.build()
.create(AuthApi::class.java)

@Provides
fun provideAuthRepository(api: AuthApi): AuthRepository =
AuthRepositoryImpl(api)

@Provides
fun provideDoLoginUseCase(repo: AuthRepository): DoLoginUseCase =
DoLoginUseCase(repo)
}

🧩 Exportar como artefacto reutilizable

Este módulo puede convertirse en un artefacto aar y compartirse en un repositorio como:

dependencies {
implementation("com.alpura.modules:login:1.0.0")
}

📌 Referencias


📦 Este módulo estará incluido en el repositorio alpura-moblie-kotlin-android-archetype-base como parte del proyecto base funcional.