Skip to main content

3. Lineamientos de Desarrollo

Este documento define los lineamientos técnicos, arquitectónicos y de buenas prácticas que deben seguirse en todos los proyectos de desarrollo móvil Android en Alpura. Su objetivo principal es:

  • Garantizar la calidad, escalabilidad, seguridad y mantenibilidad de las apps móviles.
  • Establecer una guía única de referencia para todo el ciclo de desarrollo técnico.

3.1 Alcance del documento

A quién está dirigido:

  • Desarrolladores Android (junior a senior)
  • Arquitectos de software mobile
  • Líderes técnicos de equipos de desarrollo
  • Personal de QA y DevOps relacionado con apps móviles

Aplicabilidad:

  • Proyectos nuevos
  • Refactorización o migración de proyectos existentes

Contenidos cubiertos:

  • Stack tecnológico oficial
  • Arquitectura base recomendada por Google
  • Estructura modular del proyecto
  • Prácticas de calidad, pruebas, seguridad y documentación
  • Casos de uso modelo con funcionalidades reales

Repositorio oficial de referencia:

El proyecto base funcional AlpuraMobileArchetype se debe tomar como punto de partida obligatorio para nuevos desarrollos.


3.2 Requisitos de Negocio para comenzar el desarrollo

Antes de comenzar con el desarrollo técnico de una aplicación móvil, es indispensable que se documenten, validen y aprueben los siguientes elementos por parte del equipo de negocio y producto:

🔹 Historias de usuario

  • Redactadas en formato: "Como [rol], quiero [acción], para [objetivo]"
  • Deben incluir:
    • Criterios de aceptación claros
    • Condiciones de éxito y errores esperados
    • Reglas de negocio

🔹 Flujos funcionales y UX/UI

  • Prototipos interactivos en herramientas como Figma
  • Diagramas de flujo de navegación entre pantallas
  • Validación de la experiencia de usuario con focus groups o stakeholders internos

🔹 Validación técnica inicial

  • Evaluación de factibilidad por parte del arquitecto de software
  • Revisión de restricciones técnicas, legales o de seguridad
  • Identificación de dependencias con otros sistemas

🔹 Estimación de esfuerzo

  • Técnicas sugeridas: Planning Poker, T-shirt sizing
  • Desglose por sprint o entregables parciales

🔹 Identificación de métricas

  • Definir los KPIs funcionales desde inicio (ej. tasa de conversión, tiempo de sesión, etc.)
  • Las métricas deben ser medibles con herramientas como Firebase Analytics, App Performance o APIs internas

🔹 Herramientas recomendadas:

  • Jira / Trello (para documentación y seguimiento de historias)
  • Figma / Miro (para diseño de interfaces)
  • Confluence / Notion (para actas de reuniones, decisiones y documentación técnica)

3.3 Versiones de Android Soportadas

Se establecen los siguientes valores como estándar mínimo y objetivo en todos los proyectos Android de Alpura:

ConfiguraciónValor
minSdkVersion24 (Android 7.0 Nougat)
targetSdkVersion35 (Android 15)
compileSdkVersion35 (Android 15)

🔎 Justificación técnica

  • minSdkVersion 24 permite aprovechar:

    • Soporte de Kotlin coroutines
    • Jetpack Compose con buen rendimiento
    • Eliminación de APIs obsoletas no mantenidas por Google
    • Mayor seguridad, ya que Android 7 ya incluye mejoras significativas de privacidad
  • targetSdkVersion y compileSdkVersion se mantendrán siempre en la última versión estable publicada por Google para garantizar:

    • Compatibilidad con nuevos componentes Jetpack
    • Acceso a nuevas APIs del sistema operativo
    • Cumplimiento de políticas de publicación en Google Play

📋 Lineamientos adicionales

  • El equipo de arquitectura revisará y actualizará anualmente el minSdkVersion, dependiendo del porcentaje de adopción de versiones en la base de usuarios.
  • Cualquier cambio en los valores anteriores debe justificarse con:
    • Informe de impacto en compatibilidad
    • Decisión documentada en la bitácora técnica del proyecto

📘 Referencias


3.4 🧱 Arquitectura modular

Esta sección contiene una estructura modular detallada de un proyecto base AlpuraMobileArchetype, con todos los niveles, subniveles y archivos ejemplo. Incluye navegación desacoplada, subcapas organizadas y separación por responsabilidades alineada con Clean Architecture, MVVM y SOLID, recomendados por Google para Android con Kotlin.

🔗 Repositorio oficial del proyecto base

El proyecto base completo puede encontrarse en el repositorio corporativo:

📂 Repositorio oficial:
alpura-moblie-kotlin-android-archetype-base

Este debe ser utilizado como base para cualquier desarrollo mobile nuevo. Incluye:

  • Stack tecnológico actualizado y funcional
  • Ejemplo de navegación, UI con Jetpack Compose, uso de Flow, Room, etc.
  • Pruebas unitarias y de UI con Hilt y datos simulados
  • Documentación KDoc en todos los archivos relevantes

Estructura general del proyecto base: AlpuraMobileArchetype

AlpuraMobileArchetype/
├── app/ # Módulo contenedor: punto de entrada y orquestador de navegación
├── core/ # Librerías comunes compartidas entre módulos (network, seguridad, utilidades)
└── features/ # Funcionalidades separadas por contexto (login, productos, settings)
├── login/ # Módulo reutilizable en cualquier app Alpura
├── products/ # Módulo para listar y detallar productos
└── settings/ # Módulo para configuración del usuario y preferencias

🚀 MÓDULO app/ (ORQUESTADOR PRINCIPAL)

Este módulo contiene la MainActivity, el punto de entrada, la navegación principal, y la configuración global de dependencias y tema.

app/
├── src/main/java/com/alpura/mobile/archetype/
│ ├── App.kt # Clase Application para iniciar Hilt
│ ├── MainActivity.kt # Actividad principal que inicia la UI
│ ├── navigation/ # Gestión de rutas desacopladas
│ │ └── AppNavigation.kt # NavHost con integración modular
│ ├── ui/components/ # Composables reutilizables (botones, tarjetas, etc.)
│ ├── ui/theme/ # Colores, estilos, tipografías globales
│ ├── ui/state/ # Estados compartidos entre pantallas
│ └── di/ # Módulos de Hilt compartidos

📄 App.kt

/**
* Punto de entrada de la aplicación.
* Inicializa Hilt para inyección de dependencias.
*/
@HiltAndroidApp
class MyApplication : Application()

📄 MainActivity.kt

/**
* Actividad principal de la aplicación Alpura.
* Orquesta el tema y la navegación base.
*/
@AndroidEntryPoint
class MainActivity : ComponentActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// Configuración del contenido Compose
setContent {
AlpuraTheme {
// Función que inicia la navegación principal
AppNavigation()
}
}
}
}

🔐 MÓDULO core/network/ (CLIENTE HTTP Y SEGURIDAD)

core/network/
├── RetrofitClient.kt # Construcción de instancia Retrofit global
├── LoggingInterceptor.kt # Interceptor de logging y headers
├── NetworkModule.kt # Módulo de inyección para Hilt

📄 RetrofitClient.kt

/**
* Objeto singleton para crear una instancia de Retrofit
* con configuración base y cliente HTTP personalizado.
*/
object RetrofitClient {
fun build(): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.alpura.com/") // URL base configurable
.client(
OkHttpClient.Builder()
.addInterceptor(LoggingInterceptor()) // Interceptor para logs o auth
.build()
)
.addConverterFactory(GsonConverterFactory.create()) // Conversor JSON
.build()
}
}

👤 MÓDULO features/login/ (MODULAR Y REUTILIZABLE)

Este módulo gestiona el flujo de autenticación de usuarios mediante email y contraseña. Está diseñado para reutilizarse en diferentes aplicaciones Alpura. Posee una estructura completa con UI, lógica, repositorios y conexión a API.

features/login/
├── ui/screens/LoginScreen.kt # Composable principal de la pantalla de login
├── presentation/LoginViewModel.kt # ViewModel con estados y acciones de UI
├── domain/model/Credentials.kt # Modelo de datos de credenciales
├── domain/usecase/DoLoginUseCase.kt # Caso de uso: realiza login
├── domain/repository/AuthRepository.kt # Interface del repositorio
├── data/repository/AuthRepositoryImpl.kt # Implementación del repositorio
├── data/datasource/remote/AuthApi.kt # Interfaz Retrofit de login
├── di/LoginModule.kt # Módulo de Hilt para inyección

📄 LoginViewModel.kt

/**
* ViewModel que expone los estados y eventos de la pantalla de login.
*/
@HiltViewModel
class LoginViewModel @Inject constructor(
private val doLoginUseCase: DoLoginUseCase
) : ViewModel() {

var uiState by mutableStateOf(LoginUiState())
private set

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

🛒 MÓDULO features/products/ (PRODUCTOS)

Este módulo representa un flujo común de catálogo de productos, con listado, detalle y almacenamiento local/cache.

features/products/
├── ui/screens/ProductListScreen.kt # Lista de productos
├── presentation/ProductViewModel.kt # ViewModel que gestiona estado
├── domain/model/Product.kt # Entidad de producto
├── domain/usecase/GetProductsUseCase.kt # Caso de uso
├── domain/repository/ProductRepository.kt # Interface repositorio
├── data/repository/ProductRepositoryImpl.kt # Implementación repositorio
├── data/datasource/remote/ProductApi.kt # API Retrofit
├── data/datasource/local/ProductDao.kt # DAO Room
├── data/mapper/ProductMapper.kt # Mapper DTO -> Entidad
├── di/ProductModule.kt # Inyección con Hilt

⚙️ MÓDULO features/settings/ (PREFERENCIAS)

Este módulo administra configuración visual, preferencias de usuario, y cambios persistentes locales mediante DataStore.

features/settings/
├── ui/screens/SettingsScreen.kt # Pantalla de configuración
├── presentation/SettingsViewModel.kt # ViewModel
├── domain/model/UserPreferences.kt # Modelo de preferencias
├── domain/usecase/UpdateThemeUseCase.kt # Caso de uso
├── domain/repository/SettingsRepository.kt # Interface
├── data/repository/SettingsRepositoryImpl.kt # Implementación
├── data/datasource/local/PreferencesDataStore.kt # DataStore
├── di/SettingsModule.kt # Hilt DI

🔁 Patrón Común Aplicado
Todos los módulos presentan esta organización:

  • ui/screens/ → Composables principales
  • presentation/ → ViewModels
  • domain/ → Lógica de negocio y modelos
  • data/ → Conexión a fuentes, mapeo, API
  • di/ → Hilt bindings

📌 Cada módulo se puede testear de forma aislada y consumir como dependencia.


3.5 Organización del Código

Este apartado define las convenciones, estructuras y principios de organización del código fuente adoptados en el proyecto base AlpuraMobileArchetype. Se enfoca en mantener un código limpio, modular, mantenible y alineado con las mejores prácticas de ingeniería de software modernas para Android nativo con Kotlin.


📐 Principios SOLID aplicados a Android

El proyecto implementa los principios SOLID para garantizar una buena separación de responsabilidades y alta cohesión:

PrincipioAplicación en Android
S - Responsabilidad ÚnicaCada clase (ViewModel, UseCase, Repository) tiene una única responsabilidad claramente definida.
O - Abierto/CerradoInterfaces para repositorios y fuentes de datos permiten extender comportamiento sin modificar el código existente.
L - Sustitución de LiskovSe implementan contratos mediante interfaces, lo cual permite usar mocks o implementaciones concretas sin romper compatibilidad.
I - Segregación de InterfacesInterfaces específicas como TaskRepository, LoginRepository, evitan clases con métodos innecesarios.
D - Inversión de DependenciasSe inyectan dependencias mediante Hilt, desacoplando clases concretas de sus dependencias.

🧱 Estructura por capas y responsabilidades

A continuación se ilustra cómo está organizada la base de código del arquetipo por capas lógicas, con directorios bien definidos:

features/
└── login/
├── ui/screens/ # Composables de pantalla: LoginScreen.kt
├── presentation/ # ViewModels y estados: LoginViewModel.kt
├── domain/
│ ├── model/ # Entidades de dominio: User.kt
│ ├── usecase/ # Casos de uso: LoginWithCredentialsUseCase.kt
│ └── repository/ # Interfaces de repositorio
├── data/
│ ├── repository/ # Implementaciones de repositorio
│ ├── datasource/remote/ # API Login con Retrofit
│ └── datasource/local/ # Persistencia (DataStore o Room)
└── di/ # Módulo de inyección de dependencias del feature

🧭 Separación de recursos Android

Para mantener orden y consistencia se utiliza la siguiente convención en la carpeta res/:

RecursoUbicaciónConvención
Stringsres/values/strings.xmlIdentificadores claros por pantalla
Temasres/values/themes.xml, res/values-night/themes.xmlTemas definidos por capa visual
Coloresres/values/colors.xmlUso exclusivo de recursos, no hex directos
Dimensionesres/values/dimens.xmlMargenes, paddings centralizados
Drawable/Iconres/drawable/SVG o PNG optimizados por densidad

🧩 Organización en archivos de navegación global y modular

  • Navegación principal definida en app/navigation/AppNavigation.kt
  • Cada feature expone su NavGraph mediante Composable
  • Rutas centralizadas por constantes y sealed class

🧪 Ejemplo práctico del proyecto

// LoginViewModel.kt
@HiltViewModel
class LoginViewModel @Inject constructor(
private val loginWithCredentialsUseCase: LoginWithCredentialsUseCase
) : ViewModel() {

private val _uiState = MutableStateFlow(LoginUiState())
val uiState = _uiState.asStateFlow()

fun login(email: String, password: String) {
viewModelScope.launch {
val result = loginWithCredentialsUseCase(email, password)
_uiState.value = _uiState.value.copy(success = result)
}
}
}
// LoginWithCredentialsUseCase.kt
class LoginWithCredentialsUseCase @Inject constructor(
private val repository: LoginRepository
) {
suspend operator fun invoke(email: String, password: String): Boolean {
return repository.login(email, password)
}
}

3.6 Nombramiento de Clases, Métodos y Nomenclatura

Esta sección se centra en establecer una convención clara, consistente y extensiva para nombrar todos los elementos clave del código en proyectos Android con Kotlin. Aplicar un estándar robusto permite mejorar la mantenibilidad, facilita la revisión y hace que el onboarding de nuevos desarrolladores sea más eficiente.

🧠 Estas convenciones siguen las guías oficiales de:


📦 Convenciones Generales

ElementoConvenciónEjemplo
ClasesUpperCamelCaseLoginViewModel, MainActivity
InterfacesUpperCamelCaseUserRepository
Funciones/MétodoslowerCamelCasegetUser(), updateProfile()
VariableslowerCamelCaseuserName, isLoggedIn
ConstantesUPPER_SNAKE_CASEMAX_LENGTH, DEFAULT_TIMEOUT
ArchivosUpperCamelCase.ktLoginScreen.kt, User.kt
Backing Fields_camelCase privado_value, _isLoading
Propiedades PúblicaslowerCamelCaseisLoading, userName
Rutas Navegaciónsnake_case"login_screen"
Constantes de RutaUPPER_SNAKE_CASELOGIN_SCREEN_ROUTE

✏️ Ejemplo completo de Backing Property

class UserViewModel : ViewModel() {

// Campo privado de respaldo (backing field)
private var _userName: String = "default"

// Propiedad pública de solo lectura
val userName: String
get() = _userName

// Método para modificar el valor de forma segura
fun updateUser(name: String) {
_userName = name
}
}

🧠 Comentarios explicativos

  • _userName: es el campo mutable interno (backing property).
  • userName: propiedad pública expuesta como solo lectura.
  • updateUser(): función controlada para actualizar el valor.

🧠 Reglas de Prefijos, Sufijos y Convención Visual

✅ Prefijos sugeridos

  • Base: clases abstractas reutilizables (BaseFragment, BaseAdapter)
  • Abstract: clases abstractas de dominio (AbstractUseCase)
  • Fake, Mock, Test: para mocks y pruebas (FakeUserRepository, TestLoginViewModel)
  • I: (opcional) para interfaces en caso de ambigüedad (IRepository)

✅ Sufijos sugeridos

  • ViewModel: LoginViewModel, SettingsViewModel
  • Repository: UserRepository, AuthRepository
  • UseCase: GetUserUseCase
  • Screen, Activity, Fragment, Composable: LoginScreen, MainActivity
  • Mapper, Adapter, Converter, Validator

📁 Convención para paquetes y rutas

ElementoConvenciónEjemplo
Paqueteslowercasecom.alpura.feature.login
Navegaciónsnake_case"login_screen"
Constantes rutasUPPER_SNAKELOGIN_SCREEN_ROUTE

🧪 Convenciones para pruebas

Tipo de testSufijo sugeridoEjemplo
Unit testTestLoginViewModelTest
InstrumentadoInstrumentedTestLoginScreenInstrumentedTest
Dummy o fakeFake o StubFakeUserRepository, StubApiService

🧾 Archivos de Extensión y Utilitarios

  • Archivos de extensión: StringExtensions.kt, ContextExtensions.kt
  • Clases de constantes: Constants.kt, FirebaseKeys.kt
  • Clases auxiliares: prefijo Base, Delegate, Provider si aplica

🎯 Ejemplo modular real del proyecto base

class ProductViewModel @Inject constructor(
private val getProductsUseCase: GetProductsUseCase
) : ViewModel() {

private val _uiState = MutableStateFlow(ProductUiState())
val uiState: StateFlow<ProductUiState> = _uiState

fun loadProducts() {
viewModelScope.launch {
val products = getProductsUseCase()
_uiState.value = _uiState.value.copy(products = products)
}
}
}

🔍 Comentarios:

  • Uso de @Inject para constructor del ViewModel.
  • _uiState: backing field mutable.
  • uiState: observable solo lectura.
  • loadProducts(): función expresiva con verbo.

📚 Recursos


3.7 Versiones del mismo aplicativo mobile

En este apartado se definen las estrategias recomendadas para gestionar diferentes versiones de una misma aplicación Android utilizando mecanismos como productFlavors, variantes de compilación, uso de BuildConfig, y recursos diferenciados. Esto es especialmente útil para ambientes dev, qa y producción, donde se requieren distintas configuraciones sin duplicar el código base.


🎯 Objetivo

  • Mantener una sola base de código que permita múltiples versiones de la app.
  • Definir entornos con configuraciones, endpoints, assets y nombres distintos.
  • Permitir despliegue y pruebas independientes por ambiente.

🏗️ ¿Qué son los productFlavors?

Los productFlavors permiten definir diferentes variantes de una app, modificando atributos como el applicationId, versionName, o recursos.


🧱 Estructura típica de Flavors

android {
...
flavorDimensions += "environment"

productFlavors {
dev {
dimension = "environment"
applicationIdSuffix = ".dev"
versionNameSuffix = "-dev"
resValue("string", "app_name", "Arquetipo Alpura [DEV]")
buildConfigField("String", "BASE_URL", ""https://api-dev.alpura.com"")
}
qa {
dimension = "environment"
applicationIdSuffix = ".qa"
versionNameSuffix = "-qa"
resValue("string", "app_name", "Arquetipo Alpura [QA]")
buildConfigField("String", "BASE_URL", ""https://api-qa.alpura.com"")
}
prod {
dimension = "environment"
resValue("string", "app_name", "Arquetipo Alpura")
buildConfigField("String", "BASE_URL", ""https://api.alpura.com"")
}
}
}

🔧 Configuraciones adicionales por Flavor

Diferentes íconos por ambiente

Puedes colocar íconos diferenciados en:

src/dev/res/mipmap/
src/qa/res/mipmap/
src/prod/res/mipmap/

Valores específicos por entorno

Puedes sobrescribir valores como:

src/dev/res/values/strings.xml
src/qa/res/values/strings.xml

🧪 Integración con el proyecto AlpuraMobileArchetype

En el proyecto arquetipo AlpuraMobileArchetype, se implementa esta estrategia para facilitar:

  • Pruebas automatizadas en QA con build qaDebug
  • Publicación interna del flavor prodRelease
  • Despliegue de APKs diferenciados desde el pipeline

🛠️ ¿Qué puedes hacer con BuildConfig?

BuildConfig permite acceder a configuraciones directamente desde el código:

val baseUrl = BuildConfig.BASE_URL

También se puede usar para habilitar o deshabilitar funcionalidades:

if (BuildConfig.DEBUG) {
Timber.plant(DebugTree())
}

🧪 Testing por flavor

Puedes incluso definir tests específicos en rutas como:

src/devTest/
src/qaTest/

Para incluir mocks o comportamientos simulados exclusivos de cada entorno.


🧩 Complemento en Gradle

En proyectos con CI/CD, puedes usar comandos como:

./gradlew assembleQaRelease
./gradlew installDevDebug

✅ Beneficios

  • Simplificación del mantenimiento
  • Configuraciones desacopladas
  • Ciclos de QA y validación por entorno
  • Facilidad de entrega continua

Este enfoque es esencial para entornos empresariales donde los flujos de desarrollo y pruebas requieren ambientes completamente separados con control total desde Gradle.


3.8 Solicitud de Permisos

La solicitud de permisos en Android es un aspecto crítico que garantiza la privacidad del usuario. Este punto define el enfoque correcto para implementar y gestionar permisos en tiempo de ejecución, alineado con la arquitectura moderna y los principios de UX y seguridad.


🔐 Tipos de permisos

Android clasifica los permisos en:

  • Normales: acceso a internet, estado de red, etc.
  • Sensibles: ubicación, almacenamiento, cámara, micrófono.

A partir de Android 6 (API 23), los permisos sensibles deben solicitarse en tiempo de ejecución.


📲 Permisos en AndroidManifest.xml

Se deben declarar los permisos requeridos en el archivo AndroidManifest.xml:

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

✅ Enfoque moderno con ActivityResultContracts

Se recomienda usar el enfoque basado en contratos de resultados para solicitar permisos desde Jetpack Compose o actividades/fragments modernos.

val cameraPermissionLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
// Permiso otorgado
startCamera()
} else {
// Permiso denegado
showPermissionDeniedMessage()
}
}

Ejemplo de uso

Button(onClick = {
cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
}) {
Text("Tomar Foto")
}

📦 Gestión desde ViewModel

Se recomienda delegar al menos la decisión del estado de permiso al ViewModel:

class CameraViewModel : ViewModel() {
var isCameraPermissionGranted by mutableStateOf(false)
private set

fun updatePermission(granted: Boolean) {
isCameraPermissionGranted = granted
}
}

🧩 Consideraciones en Compose

Recuerda que Compose no proporciona contexto directamente, por lo tanto:

  • Usa LocalContext.current si es necesario
  • Lanza los permisos desde la UI y comunica al ViewModel

📋 Buenas prácticas

  • Mostrar racional de permisos antes de solicitarlo si fue denegado anteriormente.
  • Evitar múltiples solicitudes en cadena.
  • Solicitar permisos justo antes de usarlos, no al abrir la app.

🧪 Pruebas con permisos

Para pruebas instrumentadas (androidTest), puedes usar:

@Rule
@JvmField
val permissionRule: GrantPermissionRule = GrantPermissionRule.grant(
Manifest.permission.CAMERA
)

3.9 Push y Notificaciones Locales

Esta sección describe cómo implementar correctamente las notificaciones push (visibles y silenciosas) y notificaciones locales en aplicaciones Android usando Firebase Cloud Messaging (FCM). También se incluye el manejo de notificaciones silenciosas para ejecutar tareas en segundo plano sin mostrar alertas al usuario, siguiendo la arquitectura definida en el arquetipo de Alpura.


🚀 Firebase Cloud Messaging

Firebase Cloud Messaging (FCM) permite enviar notificaciones y mensajes a clientes Android de manera segura y confiable.

📲 Tipos de Notificaciones

  1. Notificaciones visibles (Display Notifications):

    • Muestran una alerta visual en la pantalla del dispositivo (barra de estado, banner, etc).
    • Utilizadas para interacción directa con el usuario.
  2. Notificaciones silenciosas (Data Messages):

    • No muestran UI.
    • Disparan el BroadcastReceiver o FirebaseMessagingService para manejar lógica interna (sin UI).

🔧 Configuración Básica

1. Agrega Firebase al proyecto

  1. Ve a Firebase Console
  2. Crea un proyecto → Agrega Android app
  3. Descarga el archivo google-services.json y colócalo en app/
// build.gradle.kts (app)
plugins {
id("com.google.gms.google-services")
}

dependencies {
implementation(libs.firebase.messaging) // Firebase Cloud Messaging
}
// build.gradle.kts (root)
dependencies {
classpath("com.google.gms:google-services:4.4.1") // Plugin de servicios de Google
}

🔔 Ejemplo: Notificación Visible (normal)

Estas notificaciones aparecen automáticamente si se envía un mensaje con notification desde Firebase.

{
"to": "<device_token>",
"notification": {
"title": "Nueva tarea asignada",
"body": "Inspección de ganado en potrero A"
},
"priority": "high"
}

// Este JSON muestra un mensaje push visible. Se muestra en la UI automáticamente si la app está en segundo plano o cerrada.


🧩 Ejemplo: Notificación Silenciosa

Estas permiten ejecutar lógica sin alertar visualmente al usuario.

{
"to": "<device_token>",
"data": {
"type": "update_status",
"taskId": "ABC123"
},
"priority": "high",
"content_available": true
}

// Este mensaje activa el onMessageReceived() pero no genera UI visual. Ideal para sincronización en background.


📡 Implementación en FirebaseMessagingService

@HiltAndroidApp
class MyFirebaseMessagingService : FirebaseMessagingService() {

override fun onMessageReceived(remoteMessage: RemoteMessage) {
// 🔍 Identificar tipo de mensaje
remoteMessage.data["type"]?.let { type ->
when (type) {
"update_status" -> handleSilentUpdate(remoteMessage)
else -> showNotification(remoteMessage)
}
}
}

private fun handleSilentUpdate(remoteMessage: RemoteMessage) {
// 📦 Lógica de negocio sin UI (sin notificación visual)
val taskId = remoteMessage.data["taskId"]
Timber.d("Actualizar estatus de tarea: $taskId")
// TODO: Actualizar base local, sincronizar con backend, etc.
}

private fun showNotification(remoteMessage: RemoteMessage) {
// 🎨 Construcción de notificación visible
val title = remoteMessage.notification?.title ?: "Alpura"
val message = remoteMessage.notification?.body ?: "Mensaje nuevo"
val builder = NotificationCompat.Builder(this, "default_channel")
.setContentTitle(title)
.setContentText(message)
.setSmallIcon(R.drawable.ic_notification)
.setPriority(NotificationCompat.PRIORITY_HIGH)

with(NotificationManagerCompat.from(this)) {
notify(1001, builder.build())
}
}
}

🔐 Canal de Notificaciones (Android 8+)

fun createNotificationChannel(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
"default_channel",
"Canal Alpura",
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "Canal para notificaciones generales"
}

val manager: NotificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
manager.createNotificationChannel(channel)
}
}

📝 Consideraciones Adicionales

  • Registrar el token del dispositivo con tu backend usando FirebaseMessaging.getInstance().token.
  • Manejar la actualización de tokens en onNewToken.
  • Silenciosas: usar data sin notification.
  • Usar canal de notificación para evitar errores en Android 8+.
  • Las notificaciones silent se deben probar con la app en segundo plano o cerrada.

3.10 Dependencias Recomendadas

Esta sección documenta las librerías, SDKs y dependencias de uso aprobado dentro de los proyectos Android desarrollados en Kotlin para Alpura. Todas las versiones listadas son compatibles con el stack tecnológico del proyecto base alpura-mobile-kotlin-android-archetype-base.


🧱 Reglas Generales

  • Usar solo versiones estables y compatibles entre sí.
  • Toda dependencia debe estar declarada en libs.versions.toml.
  • Las actualizaciones deben ser probadas en ramas qa.
  • Se priorizan librerías mantenidas por Google, JetBrains, o empresas de confianza (Square, GitHub, etc.).
  • Para librerías nuevas, justificar uso y validar compatibilidad antes de incorporarlas.

Las dependencias se clasifican por funcionalidad. Todas las versiones están declaradas en libs.versions.toml del proyecto base.

CategoríaLibreríasJustificación / Uso
UIandroidx.compose.*, material3, lifecycle-viewmodel-compose, navigation-composeConstrucción de interfaces modernas, navegación declarativa
Inyección de Dependenciashilt-android, hilt-navigation-compose, javax.injectInyección modular con Hilt y compatibilidad Compose
Persistenciaroom-runtime, room-ktx, datastore-preferencesAcceso local seguro y asincrónico
Red y APIretrofit, okhttp, gson, logging-interceptorLlamadas REST seguras y trazables
Telemetríafirebase-analytics, firebase-crashlytics, firebase-perfObservabilidad, monitoreo de fallos, performance
Push Notificationsfirebase-messagingNotificaciones estándar y silent
Seguridadandroidx.security.cryptoAlmacenamiento cifrado en local
Testeojunit, mockk, truth, androidx.compose.ui.test, hilt-testingPruebas unitarias, instrumentadas y mock de dependencias
DocumentaciónKDocGeneración automática de documentación KDoc

📋 Tabla de Librerías Aprobadas

CategoríaLibreríaVersiónEnlace OficialUso principal
UIJetpack Compose1.6.5ComposeInterfaz declarativa moderna
Material31.2.0Material3Componentes visuales de UI
NavigationNavigation Compose2.7.7Nav ComposeNavegación desacoplada
NetworkingRetrofit2.9.0RetrofitCliente HTTP para APIs REST
OkHttp4.12.0OkHttpInterceptores y capa de red
Gson2.10.1GsonSerialización y parseo de JSON
PersistenciaRoom2.6.1RoomBase de datos local
DataStore1.1.0DataStoreAlmacenamiento de preferencias
SeguridadJetpack Security Crypto1.1.0-alpha06SecurityAlmacenamiento encriptado
InyecciónHilt (Dagger)2.48HiltInyección de dependencias
FirebaseFirebase BoM32.7.3BoMUnifica versiones de librerías
Crashlytics, Analytics, Messaging, Remote Config, PerformanceIncluidas en BoMTelemetría, monitoreo, métricas
TesteoJUnit 55.10.1JUnit5Pruebas unitarias
MockK1.13.7MockKMocking para pruebas
Espresso3.5.1EspressoPruebas de UI instrumentadas
Compose UI TestIncluidaCompose TestPruebas declarativas

🧪 Ejemplo: Declaración en libs.versions.toml

# ------------------------------------------------------------------------------
# Archivo: libs.versions.toml
# Propósito: Centralizar las versiones y definiciones de dependencias y plugins
# para mantener consistencia en todo el proyecto modular Android.
# ------------------------------------------------------------------------------

[versions]
# Versión del plugin Gradle para Android
androidGradlePlugin = "8.8.2"
# Versión del core de AndroidX con extensiones Kotlin
androidxCore = "1.16.0"
# Ciclo de vida de componentes (ViewModel, LiveData, etc.)
androidxLifecycle = "2.8.7"
# Soporte para actividades y Compose
androidxActivity = "1.10.1"
# BOM de Jetpack Compose para mantener compatibilidad
androidxComposeBom = "2025.04.00"
# Integración de Hilt con Compose
androidxHilt = "1.2.0"
# Navegación con Jetpack Compose
androidxNavigation = "2.8.9"
# Persistencia de datos local con Room
androidxRoom = "2.7.0"
# Core de pruebas instrumentadas de AndroidX
androidxTest = "1.6.1"
# Core de pruebas instrumentadas de AndroidX
androidxTestExt = "1.2.1"
# Core de pruebas instrumentadas de AndroidX
androidxTestRunner = "1.6.2"
# Corrutinas de Kotlin para asincronía
coroutines = "1.10.2"
# Hilt para inyección de dependencias
hilt = "2.56.2"
# JUnit para pruebas unitarias
junit = "4.13.2"
# Versión del compilador Kotlin
kotlin = "2.1.20"
# Kotlin Symbol Processing (KSP)
ksp = "2.1.20-2.0.0"

# Versión del BOM de Firebase: asegura compatibilidad entre todos los servicios de Firebase.
firebaseBom = "32.7.1"

# Versión de Retrofit, librería para consumo de servicios HTTP REST.
retrofit = "2.9.0"

# Versión de OkHttp, cliente HTTP eficiente utilizado por Retrofit.
okhttp = "4.12.0"

# Versión de DataStore, almacenamiento de preferencias moderno basado en Flow.
datastore = "1.0.0"

# Versión de Tink, librería de Google para cifrado seguro.
tink = "1.11.0"

# Versión de SQLCipher, extensión cifrada de SQLite para Android.
sqlcipher = "4.5.4"
espressoCore = "3.6.1"
appcompat = "1.7.0"
material = "1.12.0"

[libraries]
# Kotlin Extensions para Android Core
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidxCore" }
# Activity integrada con Compose
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidxActivity" }
# BOM de Compose (no contiene código, asegura versiones compatibles)
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "androidxComposeBom" }
# Componentes de Material Design 3 con Compose
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3"}
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui"}
# Herramientas para previsualización de UI Compose
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview"}
# Pruebas de UI con JUnit 4 en Compose
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4"}
# Herramientas de diseño para Compose
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling"}
# Manifesto para pruebas de UI en Compose
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest"}
# Navegación con Hilt en Compose
androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "androidxHilt" }
# ViewModel integrado con Compose
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" }
# Runtime de ciclo de vida para Compose
androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidxLifecycle" }
# Runtime de ciclo de vida con Kotlin Extensions
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidxLifecycle" }
# Navegación de Compose
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidxNavigation" }
# Motor de base de datos Room
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "androidxRoom" }
# Extensiones Kotlin para Room
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "androidxRoom" }
# Compilador de anotaciones para Room
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "androidxRoom" }
# Base para pruebas instrumentadas
androidx-test-core = { module = "androidx.test:core", version.ref = "androidxTest" }
# Extensión JUnit para pruebas instrumentadas
androidx-test-ext-junit = { module = "androidx.test.ext:junit", version.ref = "androidxTestExt" }
# Ejecutor de pruebas instrumentadas
androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidxTestRunner" }
# Cliente Hilt para inyección de dependencias
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
# Cliente Hilt para inyección de dependencias
hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" }
# Cliente Hilt para inyección de dependencias
hilt-android-testing = { module = "com.google.dagger:hilt-android-testing", version.ref = "hilt" }
# Compilador de Hilt
hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" }
# Cliente Hilt para inyección de dependencias
hilt-gradle-plugin = { module = "com.google.dagger:hilt-android-gradle-plugin", version.ref = "hilt" }
junit = { module = "junit:junit", version.ref = "junit" }
# Corrutinas para pruebas
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }

# ------------------------------------------------------------------------------
# 🔥 Firebase
# ------------------------------------------------------------------------------

# BOM de Firebase, asegura que todas las dependencias estén en sincronía de versiones.
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" }

# Firebase Analytics: para registrar eventos y estadísticas de uso.
firebase-analytics = { module = "com.google.firebase:firebase-analytics" }

# Firebase Crashlytics: para monitorear y reportar errores en producción.
firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics" }

# Firebase Messaging: para push notifications (normales y silenciosas).
firebase-messaging = { module = "com.google.firebase:firebase-messaging" }

# Firebase Remote Config: permite modificar comportamientos dinámicamente desde consola.
firebase-config = { module = "com.google.firebase:firebase-config" }

# ------------------------------------------------------------------------------
# 🌐 Retrofit + OkHttp
# ------------------------------------------------------------------------------

# Cliente principal de Retrofit.
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }

# Conversor de objetos JSON (basado en Gson) para Retrofit.
retrofit-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" }

# Cliente HTTP OkHttp, usado internamente por Retrofit.
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }

# Interceptor para logging de peticiones/respuestas HTTP.
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }

# ------------------------------------------------------------------------------
# 🔒 Seguridad: DataStore, Tink y SQLCipher
# ------------------------------------------------------------------------------

# DataStore Preferences: reemplazo moderno de SharedPreferences con Flow.
datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" }

# Librería de cifrado de Google, usada para cifrar claves/API tokens.
tink = { module = "com.google.crypto.tink:tink-android", version.ref = "tink" }

# Librería SQLCipher para cifrar la base de datos SQLite utilizada por Room.
sqlcipher = { module = "net.zetetic:android-database-sqlcipher", version.ref = "sqlcipher" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }


[plugins]
# Plugin de aplicación Android
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
# Plugin de librería Android
android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" }
# Plugin de compilador Compose
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
# Plugin de Kotlin para Android
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
# Plugin KAPT para anotaciones Kotlin
kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
# Kotlin Symbol Processing
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp"}
# Plugin de Gradle para Hilt
hilt-gradle = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }

# Plugin de Google Services, necesario para inicializar Firebase.
google-services = { id = "com.google.gms.google-services", version = "4.4.1" }

# Plugin de Firebase Crashlytics, permite reportar errores.
firebase-crashlytics-gradle = { id = "com.google.firebase.crashlytics", version = "2.9.9" }

💡 Comentario: Esta configuración permite mantener el control de versiones en un solo archivo, evitar conflictos y facilitar upgrades.


📦 Justificación del Stack

  • Jetpack Compose: UI declarativa moderna, facilita pruebas y separación de lógica.
  • Retrofit + OkHttp: estándar para networking con soporte a interceptores, caché y logging.
  • Room + DataStore: persistencia local robusta y moderna.
  • Firebase (BoM): integración completa con servicios de telemetría, distribución y analítica.
  • Hilt: inyección de dependencias escalable y mantenible.
  • JUnit + MockK + Espresso: cobertura de pruebas amplia y desacoplada.

3.9 Nombramiento de Componentes y Métodos

3.9.1 Estándar de Codificación Kotlin

  • Usar val por defecto
  • Evitar !!, usar null-safety
  • Archivos pequeños y autocontenidos
  • Comentarios KDoc con @param, @return, @sample

Ejemplo:

/**
* Ejecuta el login del usuario.
* @param credentials Datos de acceso.
* @return Token válido si es exitoso.
*/
suspend fun login(credentials: LoginRequest): Token

3.10 Organización del Código

  • Principios SOLID
  • Separación por capas
  • Recursos en carpetas adecuadas (res/layout, res/values, etc.)
  • Modularización futura por features

3.11 Versiones del Aplicativo

productFlavors {
dev {
applicationIdSuffix ".dev"
versionNameSuffix "-dev"
}
qa {
applicationIdSuffix ".qa"
versionNameSuffix "-qa"
}
prod {}
}

3.12 Solicitud de Permisos

val permissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted -> if (granted) enableFeature() }

3.13 Notificaciones Push y Locales

  • Push normales: visibles en UI
  • Push silenciosas: manejan eventos sin mostrar notificación
override fun onMessageReceived(remoteMessage: RemoteMessage) {
val data = remoteMessage.data
if (data["silent"] == "true") {
// manejar evento sin UI
} else {
// crear NotificationCompat.Builder
}
}

3.14 Integración con Firebase

Se integrará Firebase para:

  • Crashlytics
  • Analytics
  • Push notifications
  • Remote Config
  • App Distribution
  • Performance

Configuración:

  1. Crear proyecto en Firebase Console
  2. Descargar google-services.json
  3. Agregar al módulo app/
  4. Añadir plugin com.google.gms.google-services y dependencias necesarias

3.15 Interceptores en Retrofit para Telemetría

val interceptor = Interceptor { chain ->
val request = chain.request().newBuilder()
.addHeader("X-App-Version", BuildConfig.VERSION_NAME)
.build()
chain.proceed(request)
}
val client = OkHttpClient.Builder().addInterceptor(interceptor).build()

3.11 Manejo de Errores

🎯 Objetivo

Establecer una estrategia uniforme, robusta y mantenible para el manejo de errores en todas las capas de una aplicación Android desarrollada en Kotlin bajo la arquitectura modular definida por Alpura.

El manejo adecuado de errores asegura una mejor experiencia de usuario, facilita el debugging y fortalece la estabilidad de la aplicación.


📐 Principios Generales

  • Los errores deben propagarse correctamente entre capas.
  • El manejo de errores debe realizarse en la capa más cercana al usuario, evitando lógica duplicada en capas inferiores.
  • Debe usarse un enfoque centralizado que permita:
    • Captura de errores (try-catch)
    • Logging con herramientas como Timber
    • Reporte automatizado a Crashlytics (Firebase)
    • Comunicación de estados a la UI mediante sealed class

🧱 Patrón de Error Result (Domain + UI Layer)

/**
* Clase sellada que representa los posibles estados de una operación asíncrona.
*/
sealed class ResultState<out T> {

/**
* Estado de éxito que contiene los datos obtenidos.
*/
data class Success<out T>(val data: T) : ResultState<T>()

/**
* Estado de error que contiene la excepción lanzada.
*/
data class Error(val exception: Throwable) : ResultState<Nothing>()

/**
* Estado de carga o ejecución en curso.
*/
object Loading : ResultState<Nothing>()
}

🧠 Ejemplo en un UseCase (Domain Layer)

/**
* Caso de uso responsable de obtener una lista de productos desde el repositorio.
*/
class GetProductsUseCase @Inject constructor(
private val repository: ProductRepository // Repositorio de productos
) {
/**
* Ejecuta la operación del caso de uso.
* @return ResultState con lista de productos o excepción capturada.
*/
suspend operator fun invoke(): ResultState<List<Product>> = try {
val products = repository.getProducts() // Recupera datos
ResultState.Success(products) // Éxito
} catch (e: Exception) {
ResultState.Error(e) // Error controlado
}
}

🎛️ Comunicación con la UI (ViewModel)

@HiltViewModel
class ProductViewModel @Inject constructor(
private val getProductsUseCase: GetProductsUseCase // Caso de uso inyectado
) : ViewModel() {

// Estado mutable interno que puede ser modificado por la ViewModel
private val _state = MutableStateFlow<ResultState<List<Product>>>(ResultState.Loading)

// Estado expuesto de solo lectura para la UI
val state: StateFlow<ResultState<List<Product>>> = _state

/**
* Lógica para cargar productos y emitir el estado correspondiente.
*/
fun loadProducts() {
viewModelScope.launch {
_state.value = ResultState.Loading
_state.value = getProductsUseCase()
}
}
}

🖼️ Renderizado en la UI con Compose

@Composable
fun ProductListScreen(viewModel: ProductViewModel = hiltViewModel()) {
val state by viewModel.state.collectAsState()

when (state) {
is ResultState.Loading -> {
// Mostrar un loader o animación
CircularProgressIndicator()
}

is ResultState.Success -> {
// Mostrar lista de productos
val products = (state as ResultState.Success).data
ProductList(products = products)
}

is ResultState.Error -> {
// Mostrar error con mensaje personalizado
val error = (state as ResultState.Error).exception
Text("Error al cargar productos: ${error.localizedMessage}")
}
}
}

🌐 Manejo de Errores en Retrofit

/**
* Cliente Retrofit con manejo de errores y logging.
*/
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit =
Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()

/**
* Interceptor global que captura errores HTTP.
*/
class ErrorInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()

val response = chain.proceed(request)

if (!response.isSuccessful) {
// Puedes lanzar errores personalizados por código
throw HttpException(response)
}

return response
}
}

🔁 Mejor Práctica: Repository que transforma errores

/**
* Implementación del repositorio que transforma excepciones de red en estados de error.
*/
class ProductRepositoryImpl @Inject constructor(
private val api: ProductApi
) : ProductRepository {

override suspend fun getProducts(): List<Product> {
try {
return api.getProducts()
} catch (e: IOException) {
Timber.e(e, "Error de red")
throw e
} catch (e: HttpException) {
Timber.e(e, "Error HTTP")
throw e
}
}
}

✅ Recomendaciones Finales

  • Siempre capturar errores esperados y loguearlos.
  • No suprimir silenciosamente errores sin trazabilidad.
  • Usar Crashlytics.recordException() para errores graves.
  • Siempre informar a la UI de errores con mensajes de ayuda.

3.12 Linting y Análisis Estático

El linting y análisis estático permiten detectar errores de estilo, código inseguro, prácticas no recomendadas y posibles bugs. En este proyecto, se utilizan herramientas de análisis automático para mantener la calidad del código.


🛠️ Herramientas utilizadas

  • ktlint: Verificador y formateador de estilo de código Kotlin.
  • detekt: Analizador estático de código con reglas configurables.
  • Android Lint: Validaciones del ecosistema Android.

🧭 Paso a paso de instalación y configuración

A continuación, se explica cómo instalar y configurar cada herramienta en el proyecto base.

✅ 1. Integrar ktlint con Gradle

Paso 1: Añadir plugin en build.gradle.kts a nivel raíz

// build.gradle.kts (Project-level)
// Aplicamos el plugin de ktlint para verificar estilo de código
plugins {
id("org.jlleitschuh.gradle.ktlint") version "11.6.0"
}

Paso 2: Aplicar plugin en build.gradle.kts del módulo app

// build.gradle.kts (app-level)
// Habilitamos soporte Android y definimos la versión
ktlint {
version.set("11.6.0") // Última versión estable
android.set(true) // Soporte para proyectos Android
}

Paso 3: Ejecutar verificación y formateo

# Verifica el estilo del código
./gradlew ktlintCheck

# Corrige errores de estilo automáticamente
./gradlew ktlintFormat

✅ 2. Integrar detekt para análisis estático

Paso 1: Agregar plugin de Detekt en build.gradle.kts a nivel raíz

// build.gradle.kts (Project-level)
plugins {
id("io.gitlab.arturbosch.detekt") version "1.23.1"
}

Paso 2: Configurar Detekt en el módulo app

// build.gradle.kts (app-level)
detekt {
// Usamos configuración personalizada si está disponible
config = files("$rootDir/config/detekt/detekt.yml")
buildUponDefaultConfig = true // Heredamos del config por defecto
}

Paso 3: Crear archivo de configuración detekt.yml

# config/detekt/detekt.yml
style:
MaxLineLength:
active: true
value: 120
MagicNumber:
active: true
ignoreNumbers: [-1, 0, 1, 2]

naming:
FunctionNaming:
functionPattern: '^([a-z]|[a-z][a-zA-Z0-9]*)$'

Paso 4: Ejecutar Detekt

# Escanea todos los archivos Kotlin para detectar problemas
./gradlew detekt

✅ 3. Configurar Android Lint

Paso 1: Crear archivo lint.xml en app/

<!-- lint.xml -->
<lint>
<issue id="MissingTranslation" severity="ignore"/>
<issue id="ContentDescription" severity="warning"/>
</lint>

Esto permite personalizar qué reglas Android Lint deben ignorarse o ajustarse en severidad.

Paso 2: Ejecutar lint manualmente

./gradlew lint

También se ejecuta automáticamente con assembleDebug o al compilar el proyecto desde Android Studio.


🔁 Integración continua

Para que estas herramientas se ejecuten automáticamente:

# .github/workflows/ci.yml
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK
uses: actions/setup-java@v3
with:
java-version: '17'
- name: Run detekt
run: ./gradlew detekt
- name: Run ktlint
run: ./gradlew ktlintCheck

3.13 Estándares de Calidad

Este punto establece los criterios que todo proyecto Android en Kotlin debe cumplir en términos de calidad de código, cobertura de pruebas, procesos de revisión y análisis estático. Está diseñado para garantizar mantenibilidad, confiabilidad y prevención de errores en entornos productivos.


🎯 Objetivos

  • Asegurar un nivel mínimo de calidad y consistencia en todos los módulos del proyecto.
  • Establecer reglas claras para pruebas, revisión de código y análisis estático.
  • Integrar herramientas automáticas de validación de calidad.
  • Mejorar la detección temprana de errores y vulnerabilidades.

✅ Cobertura de Pruebas

Todo proyecto debe mantener una cobertura mínima del 85% en pruebas unitarias. Esta cobertura debe contemplar:

  • Casos de uso (usecases/)
  • ViewModels
  • Repositorios (mockeados)
  • Validadores y funciones utilitarias

Herramientas recomendadas

Ejemplo de test unitario usando JUnit y MockK

@OptIn(ExperimentalCoroutinesApi::class)
class GetTasksUseCaseTest {

// Simulamos el repositorio para controlar su comportamiento
private val repository: TaskRepository = mockk()

// Caso de uso bajo prueba
private lateinit var getTasksUseCase: GetTasksUseCase

@BeforeTest
fun setup() {
// Inicializamos el caso de uso con el repositorio simulado
getTasksUseCase = GetTasksUseCase(repository)
}

@Test
fun `debe retornar tareas cuando el repositorio responde correctamente`() = runTest {
// Definimos los datos simulados de respuesta
val fakeTasks = listOf(Task("1", "Demo", "Tarea demo"))
coEvery { repository.getTasks() } returns fakeTasks

// Ejecutamos el caso de uso
val result = getTasksUseCase()

// Verificamos el resultado
assertEquals(fakeTasks, result)
}
}

💬 Este test verifica que el caso de uso devuelve correctamente las tareas desde el repositorio, usando una dependencia simulada (mockk).


🔍 Análisis Estático

Se debe integrar análisis estático de código para prevenir errores antes de llegar a producción.

Herramientas recomendadas


Integración de SonarCloud con Gradle

// Plugin de SonarQube para análisis estático de código.
// Permite integrar el análisis de calidad dentro del flujo de CI/CD o local.
plugins {
id("org.sonarqube") version "4.2.1.3168" // Versión estable recomendada del plugin SonarQube
}

sonar {
properties {
// Clave del proyecto en SonarCloud. Debe coincidir con el ID registrado en la plataforma.
property("sonar.projectKey", "alpura_android_archetype")

// Organización a la que pertenece el proyecto en SonarCloud.
property("sonar.organization", "alpura")

// URL del host de SonarCloud o SonarQube local.
property("sonar.host.url", "https://sonarcloud.io")

// Directorio donde se encuentran los archivos fuente del proyecto que serán analizados.
property("sonar.sources", "src/main/java")

// Directorio que contiene los archivos de pruebas unitarias.
property("sonar.tests", "src/test/java")

// Ruta de los reportes generados por JUnit al ejecutar los tests.
// Son utilizados para medir cobertura y resultados de testeo.
property("sonar.junit.reportPaths", "build/test-results/test")
}
}

💡 Este bloque puede ir en el build.gradle.kts del proyecto o incluso en un archivo de configuración build.gradle.kts específico para análisis, dependiendo de la estrategia de CI/CD que se esté utilizando.


🧪 Política de Pull Requests

Cada PR deberá cumplir con lo siguiente:

  • ✅ Pasar todos los tests unitarios (./gradlew test)
  • ✅ Ejecutar validaciones Lint (./gradlew lint, ktlintCheck, etc.)
  • ✅ Revisado al menos por un desarrollador distinto
  • ✅ No introducir code smells ni security hotspots
  • ✅ Tener descripción clara y vinculada a historia o issue (ej. #ALPURA-321)
  • ✅ Cumplir con la convención de commits conventional commits

📋 Criterios de Aprobación

CriterioUmbral mínimoHerramienta
Cobertura de pruebas≥ 85%JUnit + MockK + Jacoco
Estilo de código100%Ktlint, Detekt
Análisis estático limpio0 erroresSonarCloud, SonarLint
PR aprobadoRevisión cruzadaGitLab Merge Request

3.14 Documentación del Código

Este apartado define los lineamientos oficiales para documentar el código fuente de las aplicaciones móviles Android escritas en Kotlin dentro del ecosistema Alpura. Toda la documentación se basa en el uso de KDoc, el formato de documentación oficial de Kotlin.

🎯 Objetivo

  • Estandarizar la forma en que se escribe documentación técnica en los proyectos Android.
  • Facilitar el onboarding y la comprensión del código por parte de cualquier desarrollador.
  • Cumplir con los criterios de calidad exigidos por herramientas como Sonar y CI/CD internas.

📘 ¿Qué es KDoc?

KDoc es el sistema de documentación para el lenguaje Kotlin, muy similar a Javadoc pero con mejoras de legibilidad. Cada comentario KDoc debe colocarse inmediatamente antes de clases, interfaces, funciones o propiedades que se desea documentar.


🛠️ Estándar Adoptado

  • Idioma: Español técnico
  • Formato: Markdown básico (listas, negritas, código inline, enlaces)
  • Cobertura: Todas las clases públicas, métodos, propiedades, interfaces y objetos
  • Ubicación: Inmediatamente antes del bloque de código a documentar
  • Inspección: Activar inspección KDoc en Android Studio → Settings → Editor → Inspections → Kotlin → KDoc

🧩 Parámetros de KDoc Permitidos

EtiquetaUso
@paramDescribe un parámetro de una función o constructor
@returnDescribe el valor devuelto por una función
@throwsDescribe excepciones que puede lanzar la función
@sampleReferencia un ejemplo de uso concreto
@constructorExplica el constructor de una clase
@propertyDescribe una propiedad expuesta
@receiverEn funciones de extensión, describe el receptor
@seeReferencia cruzada con otra clase, método o función relacionada
@sinceFecha de creación o versión en la que se introdujo
@authorAutor responsable del código/documentación

✅ Ejemplo completo de documentación con KDoc

/**
* ViewModel responsable de manejar la lógica de presentación de la pantalla de tareas.
*
* Esta clase se comunica con la capa de dominio a través del caso de uso [GetTasksUseCase],
* y expone un estado observable [uiState] para ser consumido desde la UI.
*
* @constructor Inyectado por Hilt para acceder al caso de uso correspondiente.
* @property getTasksUseCase Caso de uso para obtener tareas desde la capa de dominio.
* @sample com.alpura.mobile.archetype.ui.screens.TaskListScreen
* @since 2025-04-10
* @author Equipo Android
*/
@HiltViewModel
class TaskViewModel @Inject constructor(
private val getTasksUseCase: GetTasksUseCase
) : ViewModel() {

/**
* Estado de la UI expuesto como flujo observable.
* Contiene lista de tareas o errores desde la fuente de datos.
*
* @see TaskUiState
*/
private val _uiState = MutableStateFlow<TaskUiState>(TaskUiState.Loading)

/**
* Estado inmutable que observa la UI.
*/
val uiState: StateFlow<TaskUiState> = _uiState

/**
* Función que carga las tareas de forma asíncrona.
*
* @throws Exception si ocurre un error inesperado durante la ejecución.
*/
fun loadTasks() {
viewModelScope.launch {
try {
_uiState.value = TaskUiState.Loading
val tasks = getTasksUseCase()
_uiState.value = TaskUiState.Success(tasks)
} catch (e: Exception) {
_uiState.value = TaskUiState.Error("No se pudieron cargar las tareas")
}
}
}
}

💡 Convenciones internas

  • Todas las clases públicas deben tener un comentario de clase
  • Todas las funciones deben tener @param, @return y @throws si aplica
  • En funciones de extensión se debe documentar el @receiver
  • Si una clase es un ViewModel, UseCase o Repository, debe indicarse en la descripción
  • Si la clase o función es inyectada por Hilt, debe mencionarse

🧪 Validación Automática

  • ktlint debe configurarse con --reporter checkstyle para revisión en CI
  • Se debe usar la opción verifyKDoc() en revisión de PRs
  • Android Studio validará automáticamente la presencia de KDoc si se activa la inspección

🧰 Herramientas de Soporte

HerramientaUso
Android StudioInspección automática de KDoc
KtlintValidación en CLI o CI
SonarLintDetección de código sin documentación
SonarCloudRevisión de cobertura de documentación

🛡️ KDoc + Seguridad

En clases sensibles o con lógica crítica (por ejemplo, cifrado o autenticación):

  • Explicar el flujo completo
  • Documentar posibles errores esperados
  • Referenciar módulos relacionados
  • Explicitar si se interactúa con librerías externas o nativas

🧪 Casos reales del arquetipo

LoginViewModel.kt

/**
* ViewModel del módulo de login reutilizable.
* Se comunica con el caso de uso `DoLoginUseCase` para autenticar al usuario.
*
* @constructor Inyección vía Hilt.
* @since 2025-04-10
*/
@HiltViewModel
class LoginViewModel @Inject constructor(
private val doLoginUseCase: DoLoginUseCase
) : ViewModel() {
// Comentarios para cada propiedad y método como se mostró antes
}

3.15.1 Almacenamiento Seguro con DataStore + Tink

El almacenamiento de datos sensibles localmente en Android debe realizarse de manera segura. La combinación de DataStore + Tink de Google representa actualmente la mejor práctica, reemplazando enfoques como EncryptedSharedPreferences que ya están siendo desaconsejados por Google.

🧰 ¿Qué es Tink?

Tink es una librería criptográfica oficial de Google, diseñada para ofrecer:

  • Cifrado simétrico y asimétrico
  • Firma digital
  • Autenticación
  • Facilidad de uso y configuración segura por defecto

🔗 Referencia oficial: https://developers.google.com/tink

🧠 ¿Cuándo usar DataStore + Tink?

  • Cuando se requiere guardar información sensible como:
    • Tokens de sesión
    • Credenciales
    • Preferencias que revelan datos de usuario
  • Cuando se desea evitar el riesgo de ingeniería inversa con datos planos.
  • Cuando se desea reemplazar EncryptedSharedPreferences.

⚙️ Paso a paso: Configuración y Uso

1️⃣ Agregar dependencias necesarias

En libs.versions.toml

# DataStore Preferences
datastore-preferences = "androidx.datastore:datastore-preferences:1.0.0"

# Tink (cifrado)
tink = "com.google.crypto.tink:tink-android:1.6.1"

En build.gradle.kts

dependencies {
// DataStore Preferences
implementation(libs.datastore.preferences)

// Tink para cifrado
implementation(libs.tink)
}

💬 Estas dependencias permiten utilizar DataStore para almacenar datos clave-valor y cifrarlos automáticamente usando Tink.

2️⃣ Crear el archivo de claves cifradas con Tink

object SecureDataStore {

private const val DATASTORE_FILE_NAME = "secure_prefs.pb"
private const val MASTER_KEY_ALIAS = "master_key_tink"

fun createEncryptedDataStore(context: Context): DataStore<Preferences> {
AeadConfig.register()

val keysetHandle = AndroidKeysetManager.Builder()
.withKeyTemplate(AesGcmKeyManager.aes256GcmTemplate())
.withSharedPref(context, "tink_keyset", MASTER_KEY_ALIAS)
.withMasterKeyUri("android-keystore://$MASTER_KEY_ALIAS")
.build()
.keysetHandle

val aead = keysetHandle.getPrimitive(Aead::class.java)

return PreferenceDataStoreFactory.create(
produceFile = {
File(context.filesDir, DATASTORE_FILE_NAME)
},
corruptionHandler = ReplaceFileCorruptionHandler {
emptyPreferences()
},
serializer = EncryptedPreferencesSerializer(aead)
)
}
}

3️⃣ Crear EncryptedPreferencesSerializer.kt

class EncryptedPreferencesSerializer(
private val aead: Aead
) : Serializer<Preferences> {

override val defaultValue: Preferences = emptyPreferences()

override suspend fun readFrom(input: InputStream): Preferences {
return try {
val decryptedBytes = aead.decrypt(input.readBytes(), null)
val decoded = PreferencesProto.Preferences.parseFrom(decryptedBytes)
PreferencesMapCompat.decode(decoded)
} catch (e: Exception) {
throw CorruptionException("Error al desencriptar DataStore", e)
}
}

override suspend fun writeTo(t: Preferences, output: OutputStream) {
val encoded = PreferencesMapCompat.encode(t)
val encrypted = aead.encrypt(encoded.toByteArray(), null)
output.write(encrypted)
}
}

4️⃣ Ejemplo de uso en ViewModel

@HiltViewModel
class SettingsViewModel @Inject constructor(
@ApplicationContext context: Context
) : ViewModel() {

private val secureDataStore = SecureDataStore.createEncryptedDataStore(context)

private val themeKey = stringPreferencesKey("app_theme")

val currentTheme: Flow<String> = secureDataStore.data
.map { preferences ->
preferences[themeKey] ?: "light"
}

fun saveTheme(value: String) {
viewModelScope.launch {
secureDataStore.edit { preferences ->
preferences[themeKey] = value
}
}
}
}

🔗 Referencias oficiales


🔐 Uso de BuildConfig y secrets.properties

Esta sección detalla cómo separar y proteger información sensible y variables de entorno durante el desarrollo de aplicaciones móviles en Android, utilizando BuildConfig y secrets.properties.


🎯 Propósito

Separar las variables de entorno y secretos del código fuente para evitar que información confidencial sea expuesta en el repositorio o compilada directamente en el APK sin control.


🧱 ¿Qué es BuildConfig?

BuildConfig es una clase generada automáticamente por Gradle que contiene constantes accesibles desde cualquier parte del proyecto. Es útil para definir flags de compilación o constantes por entorno de desarrollo.

✅ Casos de uso recomendados

  • Flags de depuración
  • URLs base por entorno (dev, qa, prod)
  • Nombres de entorno o variantes
  • Activación de logs o telemetría

🛠️ Configuración de BuildConfig

📄 build.gradle.kts (nivel módulo app)

android {
defaultConfig {
// Define constantes accesibles desde Kotlin mediante BuildConfig
buildConfigField("String", "API_BASE_URL", "\"https://dev.api.alpura.com/v1/\"")
buildConfigField("boolean", "ENABLE_LOGS", "true")
}
}

🧪 Ejemplo en código Kotlin

val apiUrl = BuildConfig.API_BASE_URL
val showLogs = BuildConfig.ENABLE_LOGS

💬 Comentarios línea por línea:

  • buildConfigField(...): Inyecta una constante en BuildConfig.
  • "String" / "boolean": Tipo de dato de la constante.
  • "API_BASE_URL": Nombre de la constante.
  • "\"https://dev.api.alpura.com/v1/\"": Valor asignado. Se deben escapar las comillas con \".

🔒 ¿Qué es secrets.properties?

Es un archivo de texto plano que almacena secretos (tokens, API keys, claves privadas) de forma externa al código fuente y excluido del control de versiones mediante .gitignore.

⚠️ Este archivo no debe subirse a Git. Es exclusivo del entorno de desarrollo local.


📁 Ejemplo de secrets.properties

# Archivo secrets.properties (excluido del repositorio)
alpura_api_key=SuperSecretAlpuraKey123
analytics_token=TokenDeFirebase123

🔌 Lectura desde Gradle

📄 build.gradle.kts (nivel módulo app)

// Lee y expone secretos como constantes BuildConfig

val secretsFile = rootProject.file("secrets.properties")
val secretProps = java.util.Properties()

if (secretsFile.exists()) {
secretProps.load(secretsFile.inputStream())
secretProps.forEach { key, value ->
buildConfigField("String", key.toString(), "\"$value\"")
}
}

💬 Comentarios línea por línea:

  • rootProject.file(...): Busca el archivo en la raíz del proyecto.
  • java.util.Properties(): Crea un objeto para cargar las claves/valores.
  • load(inputStream()): Lee el archivo secrets.properties.
  • buildConfigField(...): Inyecta cada clave como constante de BuildConfig.

🧪 Ejemplo de uso en el código Kotlin

val apiKey = BuildConfig.alpura_api_key
val analyticsToken = BuildConfig.analytics_token

✅ Estas variables pueden ser accedidas como constantes, aunque siguen visibles en el APK final.


🛑 Consideraciones de seguridad

  • Nunca subir secrets.properties al repositorio. Añádelo al .gitignore.
  • No usar BuildConfig para datos críticos que deban permanecer totalmente ocultos. Usa cifrado adicional.
  • No subir el APK generado sin Proguard/R8 y revisión de fugas.

📦 En el proyecto AlpuraMobileArchetype

Este enfoque ya está implementado. Se encuentran:

  • Las claves sensibles en secrets.properties.
  • Constantes generales en BuildConfig.
  • Revisión automatizada de secretos mediante gradle-secrets-plugin.

📚 Recursos recomendados


Seguridad del Código: Ofuscación del Código con R8 y Proguard

Esta sección describe cómo aplicar la ofuscación de código en proyectos Android utilizando R8 y Proguard, en concordancia con las mejores prácticas recomendadas por Google. Se explican su propósito, configuración, uso en combinación con el proyecto base Alpura Mobile Archetype y ejemplos reales comentados línea por línea.


🎯 Propósito de la Ofuscación

La ofuscación tiene como objetivo reducir el tamaño del APK y dificultar la ingeniería inversa del código fuente. Se logra mediante:

  • Eliminación de código no utilizado (dead code)
  • Renombramiento de clases, métodos y campos a nombres no legibles
  • Reescritura del bytecode para hacerlo menos comprensible

🔧 Activación de R8 en un Proyecto Android

Desde Android Studio 3.4 en adelante, R8 está habilitado por defecto para builds release.

gradle.properties

# Habilita la ofuscación con R8
android.enableR8=true # ✅ R8 habilitado (default)

# Desactiva el uso de Proguard si se quiere solo R8 (opcional)
# android.enableProguard=false

🛡️ Archivo proguard-rules.pro

Este archivo se encuentra por defecto en el módulo app/. Aquí se definen reglas específicas de ofuscación, exclusiones y comportamiento personalizado.

Ejemplo básico comentado:

# Mantener la clase Application para evitar errores de inicialización
-keep class com.alpura.mobile.archetype.MyApplication { *; }

# Mantener anotaciones usadas por Hilt
-keep class dagger.hilt.** { *; }
-keep class javax.inject.** { *; }

# Evita ofuscar clases de modelos usados con Gson (para parseo de JSON)
-keep class com.alpura.mobile.archetype.domain.model.** {
<fields>;
}

# Mantiene nombres para Room (requiere nombres consistentes en SQLite)
-keep class androidx.room.** { *; }
-keep class * extends androidx.room.RoomDatabase

# Evita optimizaciones de métodos específicos que podrían romper compatibilidad
-dontoptimize

# Log de la ofuscación
-printmapping build/outputs/mapping.txt

📂 Configuración en build.gradle.kts (nivel módulo)

android {
buildTypes {
release {
isMinifyEnabled = true // 🔐 Activa la ofuscación en release
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), // 📦 Reglas estándar optimizadas
"proguard-rules.pro" // 📄 Tus reglas personalizadas
)
}
}
}

🔁 Integración con CI/CD y Validación

  • R8 genera un archivo mapping.txt que debe ser almacenado en un artefacto o bucket seguro.
  • Este archivo es requerido para desofuscar errores en herramientas como Firebase Crashlytics.

📌 Recomendaciones

PrácticaRecomendación
Librerías externasConsultar documentación para reglas específicas
Modelos JSONEvitar ofuscación para compatibilidad
TestingDeshabilitar en builds debug
ValidaciónUsar ./gradlew assembleRelease y probar exhaustivamente antes de publicar

🔗 Referencias Oficiales