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
LoginViewModely reacciona a su estado usandoStateFlow.
🧠 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
- Jetpack Compose Navigation
- DataStore + Tink - Android Security
- ReqRes API para pruebas
- Guía oficial de Modularización Android
📦 Este módulo estará incluido en el repositorio alpura-moblie-kotlin-android-archetype-base como parte del proyecto base funcional.