11¿Cómo se implementa la autenticación biométrica (Face ID / huella dactilar) con el almacenamiento seguro de la plataforma para garantizar que las claves criptográficas se desbloqueen únicamente tras una verificación biométrica exitosa?
Vincule la operación a una política de acceso a claves impuesta por el SO (sistema operativo) en lugar de confiar en un booleano de la interfaz de usuario. En iOS, proteja un elemento de Keychain o una clave privada admitida de Secure Enclave con los flags adecuados de SecAccessControl; biometryCurrentSet vincula el acceso al registro biométrico actual, mientras que userPresence también permite el código de acceso del dispositivo y no es exclusivamente biométrico. Para una clave privada, incluya la política de uso de clave privada apropiada y elija su clase de accesibilidad. En Android, cree una clave de Keystore que requiera autenticación de usuario; en el nivel de API (interfaz de programación de aplicaciones) 30 o superior, una configuración de solo biometría con autenticación por uso utiliza setUserAuthenticationParameters(0, AUTH_BIOMETRIC_STRONG). Inicialice la operación y autentíquese con un BiometricPrompt.CryptoObject, y luego complete dicha operación autenticada. La disponibilidad del respaldo por hardware/StrongBox debe verificarse, no asumirse. La invalidación de claves al cambiar el registro biométrico depende de los autenticadores configurados y de la política de claves. Gestione la cancelación, el bloqueo (lockout), la invalidación de claves y la recuperación de forma explícita sin eludir la autenticación silenciosamente.
val keyGen = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
val spec = KeyGenParameterSpec.Builder(
"biometric_auth_key",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(true)
.setInvalidatedByBiometricEnrollment(true)
.build()
keyGen.init(spec)
keyGen.generateKey()
Probar responder esta pregunta con un coach de IA
12¿Qué estrategias de almacenamiento en caché y optimizaciones de pipelines puedes implementar para reducir significativamente los tiempos de compilación de pull requests en ejecutores de CI (Continuous Integration) móviles multiplataforma?
Para reducir drásticamente los tiempos de compilación de pull requests en ejecutores de CI móviles, las optimizaciones deben centrarse en la resolución de dependencias, la caché de compilación y la ejecución selectiva. En primer lugar, implementa una caché de dependencias efectiva basada en hashes de archivos de bloqueo (por ejemplo, `package-lock.json`, `Podfile.lock`, `gradle/verification-metadata.xml` o archivos resueltos de paquetes de SPM). Esto evita descargar o volver a resolver paquetes externos en instancias limpias del ejecutor. En segundo lugar, configura cachés de compilación nativas. Para Android, habilita la Gradle Build Cache (`--build-cache`) con almacenamiento en caché HTTP remoto o caché persistente nativa de CI para `~/.gradle/caches` y directorios de compilación. Para iOS, almacena en caché `DerivedData` de Xcode y los artefactos de compilación de Swift Package / CocoaPods cuidadosamente, o aprovecha herramientas modernas de almacenamiento en caché remoto como Tuist o Bazel. Para React Native/Flutter, almacena en caché la salida del bundle de JS, `node_modules` y la caché del SDK del motor de Flutter. En tercer lugar, aplica la ejecución selectiva de trabajos (detección de cambios y análisis de impacto de pruebas). Usa filtros de rutas de Git para omitir compilaciones de iOS cuando solo cambiaron archivos de Android o de backend, ejecuta etapas rápidas de linter y pruebas unitarias antes de pruebas de UI costosas o pasos de compilación pesados, y evita generar binarios completos de producción (como AAB o IPA universales) en ejecuciones de PR donde solo se necesita una compilación de simulador/depuración o pruebas unitarias.
name: PR Fast Check
on:
pull_request:
paths:
- 'android/**'
- 'shared/**'
jobs:
android-unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
# Gradle build caching for dependencies & task outputs
- uses: gradle/actions/setup-gradle@v3
with:
cache-read-only: false
- name: Run Unit Tests & Lint Only
run: ./gradlew testDebugUnitTest lintDebug --build-cache --parallel
Probar responder esta pregunta con un coach de IA
13¿Cómo estructurarías una funcionalidad compartida para que la presentación, los casos de uso de dominio y el acceso a datos sigan siendo testeables de forma independiente y reutilizables entre iOS y Android?
Para estructurar una funcionalidad compartida de modo que permita la capacidad de prueba independiente y la reutilización entre iOS y Android, se adopta una arquitectura limpia por capas organizada en Presentación, Dominio y Datos: 1. **Capa de dominio (lógica pura compartida):** contiene entidades de negocio puras, interfaces de repositorios (contratos) y casos de uso/interactores (por ejemplo, `GetCartUseCase`, `ApplyPromoCodeUseCase`). Esta capa no tiene ninguna dependencia de frameworks de UI (UIKit, SwiftUI, Android Views, Jetpack Compose, widgets de Flutter) ni de API de plataforma. Cada caso de uso encapsula una única operación de negocio, lo que permite probarlo al 100% mediante pruebas unitarias con mocks simples. 2. **Capa de datos (acceso a datos y encapsulación):** implementa las interfaces de repositorio del dominio. Interactúa con fuentes de datos remotas (REST/GraphQL) y persistencia local (SQLite/clave-valor). Los DTO de red y las entidades de base de datos se mapean estrictamente a entidades de dominio limpias antes de salir de esta capa, lo que evita que los esquemas de serialización externos o los cambios de base de datos se filtren a las capas de dominio o de UI. 3. **Capa de presentación (UI y gestión de estado):** consta de contenedores de estado (ViewModels, Blocs o Presenters) y vistas/widgets de UI. El contenedor de estado llama a los casos de uso del dominio, gestiona el estado de la UI (cargando, éxito, error) y expone un estado observable a la vista. La vista simplemente observa este estado y lo renderiza. Esta estructura permite probar de forma unitaria los casos de uso de manera aislada sin UI, usar mocks de repositorios para probar los view models y cambiar las implementaciones de presentación entre plataformas mientras se reutiliza toda la lógica de dominio y datos.
[ UI View / Compose / SwiftUI ]
|
v (Observes State / Dispatches Events)
[ ViewModel / Presenter / BLoC ] (Presentation Layer)
|
v (Executes pure business operations)
[ Use Case / Interactor ] (Domain Layer - Pure Shared Logic)
|
v (Calls interface contract)
[ Repository Interface ] (Domain Layer Contract)
^
| (Implements)
[ Repository Implementation ] (Data Layer)
|---- Maps NetworkDTO -> DomainEntity
|---- Maps DatabaseEntity -> DomainEntity
[ API Client / Local DB / Storage ]
Probar responder esta pregunta con un coach de IA
14¿Cómo evalúas cuándo el estado local de un componente ya no es suficiente y eliges un enfoque arquitectónico adecuado de gestión de estado para una base de código multiplataforma con varios desarrolladores?
En el desarrollo móvil multiplataforma, la transición del estado local de un componente (`useState`/`StatefulWidget`) a una solución arquitectónica de gestión de estado está impulsada por el alcance del estado, los requisitos del ciclo de vida y la capacidad de prueba (testability):
1. **Cuándo el estado local es insuficiente**:
- **Compartición entre componentes y perforación de propiedades (prop drilling)**: Cuando el estado debe ser accedido o modificado a través de rutas de navegación dispares o ramas distantes del árbol de widgets/componentes.
- **Persistencia en el ciclo de vida**: Cuando los datos deben sobrevivir al desmontaje de pantallas, transiciones de rutas o reinicios de navegación (por ejemplo, sesión de usuario, contenido del carrito, feeds en caché).
- **Separación de responsabilidades y testabilidad**: Cuando la lógica de negocio, la validación y los efectos secundarios se mezclan con los componentes de la interfaz de usuario, dificultando las pruebas unitarias automatizadas desacopladas de la UI.
2. **Elegir una arquitectura para equipos con varios desarrolladores**:
- **Flujo de datos unidireccional / Contenedores predecibles (como BLoC, Redux o Riverpod)**: Impone una separación estricta donde la UI despacha eventos/acciones explícitos y renderiza el estado inmutable emitido por unidades dedicadas de lógica de negocio. Esto crea contratos claros, reduce efectos secundarios y permite realizar pruebas unitarias independientes de la lógica de negocio.
- **Almacenes reactivos atómicos / de ámbito delimitado (como Zustand, MobX o Provider)**: Ofrecen menor cantidad de código repetitivo (boilerplate) y suscripciones flexibles mediante selectores, siendo adecuados cuando los equipos buscan un desacoplamiento ligero y re-renderizados dirigidos de componentes.
En un entorno de equipo, el objetivo arquitectónico principal es aislar la lógica de negocio pura de las capas de renderizado de la UI y utilizar suscripciones selectivas para evitar re-renderizados innecesarios de todo el árbol.
// Zustand store: pure state container isolated from React UI hierarchy
import { create } from 'zustand';
interface CartState {
items: string[];
addItem: (item: string) => void;
clearCart: () => void;
}
export const useCartStore = create<CartState>((set) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
clearCart: () => set({ items: [] }),
}));
// Usage in components uses selective subscriptions to avoid unnecessary re-renders:
// const itemCount = useCartStore((state) => state.items.length);
Probar responder esta pregunta con un coach de IA
15¿Cómo investigarías y reducirías el jank en una pantalla de una aplicación móvil multiplataforma que sufre tirones al desplazarse por una lista larga de imágenes y contenido dinámico?
En primer lugar, reproduciría los tirones en dispositivos reales y realizaría un perfilado del rendimiento, ya que el jank durante el desplazamiento puede originarse por diversas causas: superación del presupuesto de tiempo por fotograma (frame budget), sobrecarga de trabajo en JS o Dart, saturación del hilo principal o de UI, renderizado intensivo en la GPU, cálculos complejos de layout, decodificación de imágenes, presión en la memoria o latencia de red. A 60 Hz la aplicación dispone de unos 16,7 ms por fotograma, y a 120 Hz de unos 8,3 ms, por lo que operaciones costosas de renderizado, decodificación, cálculo de layout o cómputos síncronos durante el scroll pueden provocar pérdidas de fotogramas. Utilizaría herramientas como Flutter DevTools, las herramientas de rendimiento de React Native o Flipper, Android Studio Profiler, Xcode Instruments y las vistas de línea de tiempo de fotogramas para identificar el cuello de botella real. Para la lista larga, me aseguraría de que utilice renderizado perezoso (lazy) o virtualización: por ejemplo, `FlatList`, `FlashList` o `RecyclerListView` en React Native, o bien `ListView.builder`, `SliverList` o constructores similares en Flutter. Utilizaría claves estables, evitaría reconstruir o rerenderizar todas las filas ante cambios de estado en componentes superiores, memorizaría los componentes de fila o selectores según corresponda, mantendría ligeras las funciones `build`/`renderItem` y trasladaría operaciones como ordenación, filtrado, parseo de JSON, formateo o procesamiento de imágenes fuera del flujo crítico de scroll. Si las dimensiones de los elementos son predecibles, proporcionaría pistas de layout como `getItemLayout` en React Native o extensiones de tamaño fijas o prototípicas en Flutter. En cuanto a las imágenes, serviría miniaturas con las dimensiones adecuadas, implementaría caché, evitaría decodificar imágenes en resolución completa para celdas pequeñas, emplearía marcadores de posición (placeholders) y carga diferida, y vigilaría la saturación de memoria derivada de manipular demasiados mapas de bits grandes. También simplificaría layouts excesivamente anidados en las filas, reduciría el uso de sombras complejas, recortes (clipping), sobregiro (overdraw) u opacidades donde fuera necesario, paginaría o agruparía datos en lotes y volvería a perfilar la aplicación para verificar la mejora en los tiempos de fotograma y la reducción de frames caídos.
1. Record a trace while scrolling on a real device.
2. Check frame timeline: are frames exceeding 16.7 ms / 8.3 ms?
3. Identify bottleneck: JS/Dart, main/UI thread, raster/GPU, image decode, memory, or network.
4. Fix the largest measured bottleneck: virtualization, image resizing/caching, row memoization, layout simplification, moving work off scroll path.
5. Re-test on low-end and target-refresh-rate devices.
Probar responder esta pregunta con un coach de IA
16¿Cómo funcionan los límites entre hilos en las llamadas a través de puentes nativos y cómo se previenen los bloqueos por salto entre hilos o las caídas de fotogramas en la UI (User Interface) cuando los módulos nativos realizan trabajo pesado en segundo plano?
Los frameworks multiplataforma utilizan convenciones de hilos específicas para las llamadas mediante puentes nativos: por ejemplo, las llamadas estándar de `MethodChannel` en Flutter llegan al hilo principal de UI de la plataforma, mientras que las llamadas tradicionales del puente de React Native se ejecutan en un hilo dedicado de JavaScript y se despachan de forma asíncrona hacia hilos nativos. Cuando un método del puente nativo se ejecuta en el hilo principal o de UI, ejecutar operaciones intensivas de CPU, operaciones síncronas prolongadas de E/S de disco o tareas bloqueantes de red detiene el bucle principal de ejecución. Esto provoca pérdida de fotogramas, tirones visibles en la UI y, en Android, errores de tipo ANR (Application Not Responding) o terminaciones por el watchdog en iOS. Para evitar esto, los manejadores del puente nativo deben delegar el trabajo pesado o bloqueante a grupos de ejecución en segundo plano (por ejemplo, Kotlin Coroutines con `Dispatchers.IO`/`Dispatchers.Default`, `ThreadPoolExecutor` en Android, o `Task.detached` / `DispatchQueue.global()` de GCD en Swift). Una vez finalizado el trabajo, el resultado debe enviarse de vuelta al hilo del puente esperado por el framework (por ejemplo, devolviendo resultados en el hilo de UI para MethodChannels estándar o usando colas de tareas en segundo plano), garantizando que las actualizaciones de estado multiplataforma no bloqueen el ciclo de renderizado de la UI.
class ImageProcessorPlugin : MethodChannel.MethodCallHandler {
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
if (call.method == "applyFilter") {
val imageBytes = call.argument<ByteArray>("image") ?: return result.error("INVALID_ARG", "Image null", null)
// Dispatch heavy computation off the main UI thread
scope.launch {
try {
val processed = processBitmapBytes(imageBytes) // CPU heavy work
withContext(Dispatchers.Main) {
result.success(processed)
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
result.error("PROCESSING_FAILED", e.localizedMessage, null)
}
}
}
} else {
result.notImplemented()
}
}
}
Probar responder esta pregunta con un coach de IA
17¿Cómo diseñaría una estrategia de almacenamiento en caché e invalidación para pantallas de listas y feeds utilizando Stale-While-Revalidate y cabeceras HTTP (Hypertext Transfer Protocol) condicionales?
Una estrategia sólida de almacenamiento en caché e invalidación para pantallas de feeds y listas combina el patrón Stale-While-Revalidate (SWR) con cabeceras de validación condicionales de HTTP (como `ETag` / `If-None-Match` o `Last-Modified` / `If-Modified-Since`). Al abrir la pantalla, esta lee y renderiza de inmediato los datos cacheados desde una base de datos local (la fuente única de verdad, como Room o Core Data/SQLite), proporcionando una carga visual instantánea en la UI. De forma simultánea, se despacha una solicitud de red en segundo plano con la cabecera `If-None-Match: <etag>`. Si el servidor responde con `304 Not Modified`, no se transfiere ningún payload, lo que valida la caché local ahorrando ancho de banda y batería. Si el servidor responde con `200 OK`, los nuevos datos y la `ETag` actualizada se escriben en la base de datos local dentro de una transacción, y los observadores reactivos de la base de datos emiten automáticamente la lista actualizada a la UI. Para la paginación, los tokens de página o los offsets se almacenan junto con los registros en caché. La invalidación se desencadena por la expiración del TTL (Time-To-Live), acciones del usuario (como pull-to-refresh omitiendo las cabeceras condicionales) o efectos secundarios de mutaciones locales (por ejemplo, crear o eliminar un elemento localmente actualiza la base de datos de manera optimista y marca los cursores de página en caché para su revalidación).
fun getFeed(): Flow<List<FeedItem>> = flow {
val cached = feedDao.getFeedItems()
if (cached.isNotEmpty()) emit(cached)
val lastEtag = feedDao.getFeedEtag()
try {
val response = api.fetchFeed(ifNoneMatch = lastEtag)
if (response.code() == 200 && response.body() != null) {
feedDao.updateFeedTransaction(response.body()!!, response.headers()["ETag"])
emit(feedDao.getFeedItems())
}
} catch (e: Exception) {
if (cached.isEmpty()) throw e
}
}
Probar responder esta pregunta con un coach de IA