🕒 Hora El Salvador: --:--:-- 🔒 Cierra hoy 8:15 PM — Queda: --:--:--
Universidad Católica de El Salvador UNICAES

Práctica B1: Persistencia y Almacenamiento No Volátil

Desarrollo de Aplicaciones Móviles I — Ing. Paolo Navarro

Semana 12 • Android SDK & Jetpack Architecture • Duración mínima: 1 HORA teoría + 1 HORA práctica
📝 Parte 1 (1h): 40 preguntas — mínimo 25 buenas para el punto 🎡 Parte 2 (1h): Ruleta de 5 retos — 1 reto asignado por estudiante

Registro de Estudiante — Práctica B1

Ingresa tus datos completos para iniciar la evaluación técnica de la Semana 12.
⏱️ Parte 1: cuestionario de 40 preguntas (mínimo 1 hora). Necesitas mínimo 25 respuestas correctas para obtener el punto.
🎡 Parte 2: ruleta de 5 retos prácticos (1 hora) en Android Studio.
Al dar clic en Iniciar Práctica Evaluada, el protocolo Anti-Fraude se activará de forma obligatoria.

Lectura de Comprensión Técnica Exhaustiva

Instrucciones: Lee atentamente el siguiente compendio técnico de la Semana 12 antes de responder el cuestionario interactivo de 40 preguntas (6 secciones, incluye 2 secciones de arrastre). Tiempo estimado mínimo: 1 hora. Para lograr el punto necesitas mínimo 25 respuestas buenas de 40. El contenido cubre la taxonomía completa de persistencia en Android, SharedPreferences, Ficheros Internos, Caché y Bases de Datos (SQLite y Room). Una vez completado el quiz, desbloquearás la Ruleta de Retos (Parte 2 — 1 hora) con 5 misiones de código en Android Studio.

Compendio Técnico de Persistencia en Android (Semana 12)

Cátedra: Desarrollo de Aplicaciones Móviles I — UNICAES

1. La Pizarra vs. El Cuaderno: Memoria RAM vs. Persistencia No Volátil

En el ecosistema Android, cada aplicación se ejecuta dentro de su propio proceso de máquina virtual (ART - Android Runtime). La memoria RAM de este proceso actúa conceptualmente como una pizarra de aula de clases: es extraordinariamente rápida, eficiente para operaciones matemáticas y refresco de interfaz (60/120 Hz), pero es intrínsecamente volátil. Si el usuario rota la pantalla, entra una llamada telefónica, el sistema operativo mata el proceso en segundo plano por presión de memoria (Low Memory Killer - LMK) o el teléfono se apaga, todo dato almacenado exclusivamente en variables de memoria (Singletons, Companion Objects o ViewModels) es borrado de manera fulminante.

Por otro lado, la persistencia no volátil equivale a un cuaderno encuadernado en tinta: se escribe en la memoria de almacenamiento secundario (Flash NAND UFS / eMMC). Los datos permanecen intactos entre reinicios del dispositivo, cierres forzados de la aplicación y rotaciones de pantalla. Para todo desarrollador móvil, la regla cardinal es: La RAM es para el estado efímero del flujo presente; el almacenamiento no volátil es la única fuente fidedigna de verdad de la aplicación.

2. Los Cinco Pilares del Almacenamiento en Android

El framework de Android categoriza las opciones de almacenamiento en cinco niveles taxonómicos según su estructura, tamaño y ciclo de vida:

  • Pilar 1 - Memoria RAM (Volátil): Colecciones en memoria, StateFlow, LiveData y variables locales. Ultrarrápida pero efímera.
  • Pilar 2 - SharedPreferences & Jetpack DataStore: Almacenamiento de pares clave-valor (Key-Value) en formato XML dentro del sandbox. Diseñado para banderas booleanas, configuraciones de usuario (modo oscuro) y tokens livianos.
  • Pilar 3 - Ficheros Internos (Internal Storage / filesDir): Archivos propietarios crudos (JSONs serializados, PDFs, fotos privadas) almacenados en /data/data/<package_name>/files/. Completamente privados para la aplicación.
  • Pilar 4 - Memoria Caché (cacheDir) y Scoped Storage: Directorio temporal donde se descargan recursos auxiliares (thumbnails de Glide/Coil, respuestas HTTP). Android puede purgarlo automáticamente cuando el almacenamiento global escasea.
  • Pilar 5 - Bases de Datos Relacionales (SQLite & Room Database): Motor relacional embebido para gestionar colecciones masivas, filtrado multicriterio, ordenamiento y relaciones complejas entre entidades.
3. Pilar 2 en Detalle: SharedPreferences y Jetpack DataStore

SharedPreferences almacena datos en un archivo físico XML ubicado en /data/data/<package_name>/shared_prefs/. Para mutar sus valores, se debe invocar a la interfaz SharedPreferences.Editor:

  • apply(): Realiza la modificación de valores en memoria de manera inmediata y programa la escritura en disco de forma asíncrona. No retorna ningún valor de estado pero jamás bloquea el Hilo Principal (Main Thread), evitando provocar congelamientos de pantalla o ANR (Application Not Responding). Es la opción recomendada en el 99% de los casos.
  • commit(): Escribe inmediatamente al archivo físico en disco de forma síncrona y bloqueante. Retorna un valor booleano indicando si la escritura fue exitosa. Si se ejecuta en el Main Thread con frecuencia, degrada severamente el rendimiento y la fluidez de la interfaz visual.

Antipatrones de SharedPreferences: Jamás deben guardarse listas masivas de objetos JSON, imágenes en base64 ni contraseñas bancarias en texto plano. En la arquitectura moderna de Jetpack, Google introdujo Preferences DataStore, reemplazando SharedPreferences con una API basada en Corrutinas de Kotlin y Kotlin Flow, que garantiza seguridad de tipos y transacciones 100% asíncronas.

4. Pilares 3 y 4: Ficheros Internos, Sandbox Linux y Scoped Storage

Android se apoya en el modelo de seguridad multiusuario del kernel de Linux. Al instalarse cada app, el sistema le asigna un identificador de usuario exclusivo (UID Linux). Todo archivo creado mediante context.openFileOutput("archivo.txt", Context.MODE_PRIVATE) o dentro de context.filesDir queda encapsulado en un Sandbox Privado. Ninguna otra aplicación puede leer, modificar ni listar esos archivos sin permisos explícitos de root.

En Kotlin profesional, la lectura y escritura de streams debe utilizar la función de extensión idiomática .use { ... }. Esta garantiza que el flujo de datos (FileOutputStream / FileInputStream) invoque automáticamente el método close() al terminar la operación, previniendo fugas graves de descriptores de archivos del sistema operativo incluso si se lanza una excepción.

Diferencia clave con cacheDir: Los archivos guardados en filesDir persisten indefinidamente hasta que el usuario desinstale la app o limpie los datos desde Configuración. En cambio, cacheDir está sujeto a limpieza automática preventiva por parte del sistema si el almacenamiento del dispositivo desciende por debajo de umbrales críticos de seguridad.

Scoped Storage (Almacenamiento Aislado): Desde Android 10 (API 29) y Android 11 (API 30), las apps ya no tienen acceso irrestricto a todo el almacenamiento compartido con WRITE_EXTERNAL_STORAGE. Para fotos y audio globales se debe emplear el MediaStore API, y para archivos arbitrarios externos, el Storage Access Framework (SAF).

5. Pilar 5: Motor SQLite y Arquitectura Moderna con Room Database

Android incluye desde su versión 1.0 el motor de base de datos relacional nativo y embebido en C: SQLite. Es un motor transaccional completo (ACID), autocontenido y sin servidor (serverless), cuyos datos residen en un único archivo .db en el almacenamiento interno.

Históricamente, el acceso a SQLite se realizaba heredando de la clase abstracta SQLiteOpenHelper, sobreescribiendo sus dos métodos fundamentales:

  • onCreate(SQLiteDatabase db): Se ejecuta una única vez en la vida de la app cuando el archivo físico de la base de datos es creado por primera vez. Aquí se ejecutan sentencias DDL como CREATE TABLE.
  • onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion): Se dispara cuando el desarrollador incrementa el número de versión entera en el constructor, permitiendo migraciones de esquema como ALTER TABLE.

Para manipular filas se utilizan objetos ContentValues (pares de columna-valor) y para leer resultados se utiliza un Cursor, el cual requiere iteración manual mediante cursor.moveToNext() y obtención por índice de columna (ej. cursor.getString(...)), sin olvidar invocar cursor.close().

La Revolución de Android Jetpack Room: La API clásica de SQLite carece de verificación de sintaxis SQL en tiempo de compilación y requiere una cantidad masiva de código repetitivo (Boilerplate). Por ello, Google desarrolló Room, la capa de abstracción oficial basada en tres componentes esenciales:

  • @Entity: Clase de datos en Kotlin (data class) que define el esquema de una tabla relacional en la base de datos.
  • @Dao (Data Access Object): Interfaz que define las operaciones de acceso a datos (SELECT, INSERT, UPDATE, DELETE). Room valida la sintaxis de las consultas SQL en tiempo de compilación; si un nombre de columna no existe, el proyecto falla antes de instalarse en el dispositivo.
  • @Database: Clase abstracta que extiende de RoomDatabase y sirve como punto principal de acceso a las conexiones subyacentes.

Regla de Hilos: Por defecto, Room prohíbe terminantemente ejecutar consultas en el Main Thread porque provocaría bloqueos de UI (lanzará una excepción IllegalStateException a menos que se usen corrutinas con funciones suspend o hilos de trabajo).

6. apply() vs commit(): El Duelo Crítico

Uno de los puntos más importantes y que más se evalúa es entender la diferencia entre apply() y commit() en el SharedPreferences.Editor. Ambos métodos confirman los cambios, pero lo hacen de manera radicalmente diferente:

  • apply() — El estándar moderno (99.9% de los casos): Actualiza la copia en memoria RAM al instante (la UI lo ve ya) y envía la escritura al disco en un hilo de fondo (background). No retorna ningún valor. Jamás congela la pantalla ni produce ANR. Es la opción recomendada para guardar preferencias de usuario, modo oscuro, tokens, etc.
  • commit() — El método legacy (0.1% de los casos): Bloquea el hilo que lo llama hasta que los bytes se escriben físicamente en la memoria Flash. Retorna un boolean indicando si la escritura fue exitosa. Si se ejecuta en el Main Thread con el disco ocupado, causa lag severo o ANR (Application Not Responding). Solo se recomienda cuando necesitas confirmar el guardado antes de continuar con un proceso crítico, y siempre fuera del UI Thread.

Regla de oro: Si llamas a commit() en el hilo principal y la memoria flash está ocupada, la pantalla se congela. Si tarda más de 5 segundos, Android mata la aplicación con un ANR. Por eso Google introdujo apply() desde Android 2.3 (API 9).

7. Los 5 Asesinos de la Memoria RAM

Todo lo que vive únicamente en variables de memoria muere cuando Android necesita recursos. Estos son los 5 escenarios donde la RAM se pierde inevitablemente:

  • 1. Rotación de pantalla: El usuario gira el teléfono → la Activity se destruye y se recrea. Los datos en variables se pierden.
  • 2. Low Memory Killer (LMK): El sistema operativo mata tu app en segundo plano porque necesita memoria para otras apps.
  • 3. Swipe para cerrar: El usuario descarta la app desde el reciente de aplicaciones.
  • 4. Reinicio del equipo: La RAM siempre se vacía completamente al apagar o reiniciar el dispositivo.
  • 5. Modo Doze / batería: El proceso se congela y se purga cuando el dispositivo entra en modo de ahorro de energía.

Test de 3 segundos: Si cierras la app y ese dato puede perderse sin que el usuario reclame → la RAM es correcto. Si el usuario espera que siga ahí → debes persistir.

8. Casos de Uso Reales de SharedPreferences

SharedPreferences es ideal para persistir datos atómicos simples que configuran el comportamiento de la app:

  • Pantalla Onboarding (isFirstRun): Mostrar un carrusel de bienvenida solo la primera vez. Si es true → abrir tutorial y cambiar a false. Si es false → ir directo al MainActivity.
  • Preferencias visuales (Theme): Modo oscuro o idioma. En el onCreate() lees el valor antes de setContentView() y aplicas el tema correcto al instante.
  • Token de sesión (JWT): Guardar el token devuelto por el servidor para inyectarlo en las cabeceras HTTP de peticiones posteriores sin pedir login de nuevo.
  • Puntaje máximo o estado rápido: Guardar el récord de un juego o si los sonidos están silenciados. Comparas y actualizas con editor.putInt("high_score", puntajeActual).apply().

Antipatrones: NO guardar listas masivas de objetos JSON, imágenes en base64, contraseñas en texto plano, ni datos que requieran consultas complejas. Para eso existen archivos, Room o DataStore.

9. Scoped Storage y el Sandbox de Linux

Android se apoya en el modelo de seguridad multiusuario del kernel de Linux. Al instalarse cada app, el sistema le asigna un identificador de usuario exclusivo (UID Linux). Todo archivo creado mediante context.openFileOutput() o dentro de context.filesDir queda encapsulado en un Sandbox Privado que ninguna otra aplicación puede leer sin permisos de root.

Desde Android 10 (API 29) y Android 11 (API 30), las apps ya no tienen acceso irrestricto a todo el almacenamiento compartido con WRITE_EXTERNAL_STORAGE. Para fotos y audio globales se debe emplear el MediaStore API, y para archivos arbitrarios externos, el Storage Access Framework (SAF).

cacheDir vs filesDir: Los archivos en filesDir persisten indefinidamente hasta que el usuario desinstale la app o limpie los datos. En cambio, cacheDir está sujeto a limpieza automática preventiva por parte del sistema si el almacenamiento del dispositivo desciende por debajo de umbrales críticos. La regla de oro: jamás guardar datos esenciales en caché; si se borran, la app debe ser capaz de re-descargarlos.

10. Room Database: Componentes y Reglas

Room es la capa de abstracción oficial de Google sobre SQLite. Sus tres componentes esenciales son:

  • @Entity: Clase de datos (data class) que define el esquema de una tabla. Ejemplo: @Entity data class User(@PrimaryKey val id: Int, val name: String)
  • @Dao (Data Access Object): Interfaz que define las operaciones de acceso a datos (@Query, @Insert, @Update, @Delete). Room valida la sintaxis SQL en tiempo de compilación; si un nombre de columna no existe, el proyecto falla antes de instalarse.
  • @Database: Clase abstracta que extiende de RoomDatabase y sirve como punto principal de acceso a las conexiones subyacentes.

Regla de hilos: Room prohíbe terminantemente ejecutar consultas en el Main Thread porque provocaría bloqueos de UI (lanzará una excepción IllegalStateException). Se deben usar corrutinas con funciones suspend o hilos de trabajo.

Ventaja sobre SQLite clásico: La API clásica de SQLiteOpenHelper carece de verificación de sintaxis SQL en tiempo de compilación y requiere una cantidad masiva de código repetitivo (boilerplate). Room elimina ambos problemas.

Bibliografía y Referencias Académicas Oficiales
  • Google Developers (2024). Android Data Storage Overview. Documentación oficial de Android. (filesDir, cacheDir y Scoped Storage).
  • Google Developers (2024). Save key-value data with SharedPreferences & DataStore. Documentación de Jetpack Architecture.
  • Google Developers (2024). Save data in a local database using Room. Documentación oficial de Android Jetpack.
  • Griffiths, D., & Griffiths, D. (2021). Head First Android Development (3rd Edition). O'Reilly Media. (Persistencia relacional con SQLiteOpenHelper y Cursors).

Evaluación Teórica — Práctica B1 (40 Puntos) ⏱️ Mínimo 1 hora | ✅ Mínimo 25 para el punto

I. Verdadero o Falso

1. La memoria RAM del proceso ART es no volátil y mantiene el estado intacto ante el reinicio del teléfono.

2. El método apply() de SharedPreferences realiza la escritura al disco de forma asíncrona sin bloquear el Hilo Principal.

3. SharedPreferences está diseñado y altamente recomendado para guardar grandes colecciones masivas de objetos JSON complejos.

4. Los ficheros creados con MODE_PRIVATE dentro de filesDir quedan protegidos por el Sandbox de Linux y no pueden ser leídos por otras apps normales.

5. En Kotlin, la función de extensión .use { } cierra automáticamente el flujo (Stream) al terminar, previniendo fugas de descriptores de archivos.

6. Los archivos almacenados en el directorio cacheDir nunca son borrados por el sistema operativo, ni siquiera si el teléfono se queda sin espacio de disco.

7. En SQLiteOpenHelper, el método onCreate() se ejecuta automáticamente cada vez que el usuario abre la aplicación.

8. Room valida la sintaxis de las consultas SQL escritas en las interfaces @Dao en tiempo de compilación.

9. Por defecto, Room permite ejecutar sin problemas consultas pesadas en el Hilo Principal (Main Thread) sin arrojar excepciones.

10. Con Scoped Storage (Almacenamiento Aislado), las aplicaciones modernas ya no tienen acceso directo e irrestricto a todo el sistema de ficheros del dispositivo.

II. Selección Múltiple

11. ¿Cuál es el formato subyacente en el que SharedPreferences almacena los pares clave-valor en el disco?

12. ¿Qué ocurre si ejecutas repetidamente SharedPreferences.Editor commit() en el Hilo Principal (UI Thread)?

13. Para guardar una imagen descargada temporalmente que servirá de thumbnail en un RecyclerView, el lugar ideal es:

14. En SQLiteOpenHelper, ¿cuándo se invoca automáticamente el método onUpgrade()?

15. Componente de Room que modela la estructura de una tabla como una clase de datos en Kotlin:

16. Componente de Room donde se definen los métodos de acceso y consultas (@Query, @Insert):

17. ¿Por qué Google recomienda modernizar SharedPreferences con Jetpack DataStore?

18. Para insertar datos de forma segura evitando inyecciones SQL en SQLite clásico se utiliza:

19. El objeto retornado al realizar una consulta con db.rawQuery() o query() en SQLite se denomina:

20. ¿Qué mecanismo del kernel de Linux asegura el aislamiento del almacenamiento interno de una aplicación?

III. Completar Espacios Exactos

21. Escribe el método asíncrono recomendado del Editor de SharedPreferences para aplicar cambios en segundo plano sin congelar la interfaz:

22. ¿Cuál es el nombre de la propiedad o método del Context para acceder al directorio de archivos privados de la aplicación? (Ej: context.___________)

23. ¿Qué nombre recibe la anotación de Room que define las operaciones y métodos de consulta SQL en una interfaz? (Incluye el @ o nombre):

24. Escribe la función de extensión idiomática de Kotlin que asegura el cierre automático de un flujo (Stream) al terminar:

25. Nombre de la clase auxiliar abstracta tradicional de Android utilizada para gestionar la creación y versiones de bases de datos SQLite:

IV. Relacionar Conceptos (Dropdowns)

Selecciona en el menú desplegable la definición correcta para cada tecnología o término técnico:

26. SharedPreferences
27. cacheDir
28. Room Database
29. SQLite
30. Linux Sandbox

V. Arrastre — Elige el resultado correcto (31–35) 🖱️ Arrastra o toca

🖱️ Cómo jugar: arrastra cada tarjeta verde hacia la definición correcta. En celular: toca una tarjeta y luego toca el recuadro destino. Para quitar, pulsa ✕. Cada acierto vale 1 punto (total 5).
🧩 Conceptos (arrástrame)
apply() — método SharedPreferences
filesDir — directorio privado
cacheDir — directorio temporal
Cursor — lectura SQLite
@Dao — anotación Room
31. Escritura asíncrona que NO bloquea el Main Thread, recomendada en el 99% de los casos.Suelta aquí el concepto correcto…
32. Ruta /data/data/<paquete>/files/ que persiste hasta desinstalar. Ideal para bitácoras privadas.Suelta aquí el concepto correcto…
33. El sistema puede purgarlo si falta espacio. Ideal para thumbnails y respuestas HTTP.Suelta aquí el concepto correcto…
34. Se itera con moveToNext() y se debe cerrar con close() para no fugar memoria.Suelta aquí el concepto correcto…
35. Interfaz Room donde Room valida el SQL en tiempo de compilación (@Query, @Insert).Suelta aquí el concepto correcto…

VI. Arrastre de Código — Elige el bloque correcto (36–40) 💻

💻 Reto: arrastra el bloque de código Kotlin al caso de uso correcto. En celular: toca el código y luego el recuadro. Cada acierto vale 1 punto (total 5).
🧩 Bloques de código
Guardar preferenciaeditor.putBoolean("dark",true); editor.apply()
Escritura seguraopenFileOutput("n.txt",MODE_PRIVATE).use{ it.write(...) }
Archivo temporalFile.createTempFile("prev_",".tmp",cacheDir)
Tabla Room@Entity data class User(@PrimaryKey val id:Int)
Consulta DAO@Query("SELECT * FROM user") suspend fun getAll()
36. Guardar modo oscuro sin congelar la UI (hilo de fondo).Suelta aquí el código correcto…
37. Guardar bitácora privada con cierre automático del stream.Suelta aquí el código correcto…
38. Crear preview efímero que el SO puede borrar sin culpa.Suelta aquí el código correcto…
39. Definir el esquema de una tabla relacional en Kotlin.Suelta aquí el código correcto…
40. Leer todos los usuarios sin bloquear, con corrutina suspend.Suelta aquí el código correcto…

0/40

PUNTOS OBTENIDOS (mínimo 25/40 para el punto)

Captura esta pantalla con tu nombre y calificación para tu reporte de entrega. Si lograste ≥25, desbloqueas la Ruleta de Retos (Parte 2) a continuación. Si no, repasa y reintenta con el docente.

Resumen de Fallos y Retroalimentación Pedagógica

Revisa los temas señalados a continuación en la lectura técnica para consolidar los conceptos clave:

    🎡 Parte 2 — Ruleta de Retos Prácticos (1 hora en Android Studio)

    Cada estudiante gira la ruleta y se le asigna 1 de 5 retos fáciles (sin SQLite: solo SharedPreferences, archivos y DataStore). Desarrolla solo tu reto asignado en Android Studio (Kotlin) y documenta con capturas + código en tu PDF final. Tiempo estimado: 1 hora.

    ✅ Entrega en clase (antes de las 8:15 PM): entregas SOLO 1 reto — el que te asignó la ruleta.
    ⏰ Si se acaba el tiempo (después de las 8:15 PM): deberás entregar LOS 5 CASOS completos en tu PDF.

    🎯 Gira para conocer tu reto

    ⚠️ SOLO 1 giro por estudiante (queda guardado). Toma captura de tu resultado como evidencia.

    📌 Disponible hasta las 8:15 PM (hora de El Salvador). Después de esa hora la práctica se bloquea.

    🌓
    Reto 1 ⭐
    Modo oscuro
    🔐
    Reto 2 ⭐
    Login simple
    📝
    Reto 3 ⭐⭐
    Mi nota en archivo
    👤
    Reto 4 ⭐⭐
    Mi perfil con foto
    ❤️
    Reto 5 ⭐⭐⭐
    Favoritos DataStore
    🤖 Uso de IA permitido: puedes apoyarte en ChatGPT, Muse, Gemini o Copilot para tu reto. Cómo usarla bien: 1) cópiale el prompt que aparece en tu reto asignado, 2) pégale tu código y pregúntale qué está mal y por qué, 3) pídele que te explique antes de darte la solución. ⚠️ Debes entender tu código: el docente puede preguntarte en clase. Incluye en tu PDF qué le pediste a la IA.
    RETO ASIGNADO

    📱 Así debe verse y comportarse tu app (demo animada):
    🔎 Conceptos clave a investigar:
      📦 Librerías a usar (agrégalas en build.gradle):
      🧭 Mini-guía para construir tu app (sigue estos pasos):
        🤖 Usa la IA a tu favor: cópiale esto (qué pedirle)
        💻 Bloque de código base (cópialo y complétalo):
        📋 Entregable obligatorio (PDF + capturas):

          📚 Los 5 retos (referencia — en clase trabajas solo tu asignado; fuera de tiempo, los 5)

          🌓 Reto 1 ⭐ FÁCIL: Modo oscuro persistente

          Paso 1: Guardar modo oscuro + carné con apply()

          Pantalla con Switch (Modo Nocturno) + EditText (carné). Al presionar Guardar, persiste con apply() y al reabrir todo sigue igual.

          🔎 Investiga: apply() vs commit(), MODE_PRIVATE, getBoolean/getString con valor por defecto.
          📦 Librerías:implementation("com.google.android.material:material:1.12.0")
          Código Kotlin de Referencia: val prefs = getSharedPreferences("Ajustes", Context.MODE_PRIVATE)
          prefs.edit().putBoolean("dark_mode", switchTheme.isChecked).putString("carne", etCarne.text.toString()).apply()
          📋 Resultado Esperado:
          Captura guardando + captura tras reinicio demostrando que Switch y texto permanecen.

          🔐 Reto 2 ⭐ FÁCIL: Login que me recuerda

          Paso 2: Sesión persistente con CheckBox "Recuérdame"

          Login con usuario + CheckBox. Si está marcado, guarda activa=true + user y al reabrir entra directo. Botón Salir borra la sesión con clear().

          🔎 Investiga: putBoolean/putString + apply(), lectura en onCreate, clear() para cerrar sesión.
          📦 Librerías:implementation("com.google.android.material:material:1.12.0")
          Código Kotlin de Referencia: getSharedPreferences("Sesion", MODE_PRIVATE).edit().putBoolean("activa", true).putString("user", etUser.text.toString()).apply()
          // Salir:
          getSharedPreferences("Sesion", MODE_PRIVATE).edit().clear().apply()
          📋 Resultado Esperado:
          Captura del login + captura entrando directo tras reinicio + captura del Salir volviendo al login.

          📝 Reto 3 ⭐⭐ FÁCIL-MEDIO: Mi nota en un archivo

          Paso 3: Guardar texto en nota.txt con .use { }

          Bloc de notas que guarda en el archivo privado nota.txt (filesDir) y lo lee al abrir. Cierra el stream siempre con .use { }.

          🔎 Investiga: filesDir, openFileOutput/openFileInput, .use { }, sandbox por UID Linux.
          📦 Librerías:implementation("com.google.code.gson:gson:2.10.1") // opcional: nota como JSON
          Sintaxis de Referencia: openFileOutput("nota.txt", Context.MODE_PRIVATE).use { it.write(etNota.text.toString().toByteArray()) }
          val texto = openFileInput("nota.txt").bufferedReader().use { it.readText() }
          📋 Resultado Esperado:
          Captura escribiendo + captura tras reinicio (la nota sigue) + Device Explorer mostrando /files/nota.txt.

          Pregunta para el PDF: ¿Qué garantiza la función .use { } aunque ocurra una excepción?

          👤 Reto 4 ⭐⭐ MEDIO: Mi perfil con foto

          Paso 4: Nombre en prefs + foto en filesDir

          Perfil que guarda el nombre en SharedPreferences y la foto como foto_perfil.jpg en filesDir. Al reabrir, muestra nombre + foto con Coil.

          🔎 Investiga: bitmap.compress() a JPG, File(filesDir,...), BitmapFactory.decodeFile(), Coil imageView.load().
          📦 Librerías:implementation("io.coil-kt:coil:2.5.0")implementation("com.google.android.material:material:1.12.0")
          Sintaxis Kotlin de Referencia: prefs.edit().putString("nombre", etNombre.text.toString()).apply()
          File(filesDir, "foto_perfil.jpg").outputStream().use { bitmap.compress(Bitmap.CompressFormat.JPEG, 90, it) }
          ivFoto.load(File(filesDir, "foto_perfil.jpg"))
          📋 Resultado Esperado:
          Captura del perfil con nombre + foto, captura tras reinicio (sigue igual) y Device Explorer con /files/foto_perfil.jpg.

          ❤️ Reto 5 ⭐⭐⭐ INTERMEDIO: Favoritos con DataStore

          Paso 5: Corazones on/off con Preferences DataStore + Flow

          Lista de 3 favoritos con corazón que se guarda en DataStore con corrutinas y se observa con Flow. Al reabrir, los corazones siguen igual.

          🔎 Conceptos clave a investigar: booleanPreferencesKey, edit() suspend, data.map + collect en lifecycleScope, ventaja de DataStore sobre SharedPreferences.
          📦 Librerías:implementation("androidx.datastore:datastore-preferences:1.1.1")implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
          Bloque de código base: val FAV = booleanPreferencesKey("fav_movil")
          lifecycleScope.launch { ds.edit { it[FAV] = true } }
          lifecycleScope.launch { ds.data.map { it[FAV] ?: false }.collect { sw.isChecked = it } }
          📋 Entregable:
          captura marcando favoritos + captura tras reinicio (siguen) + respuesta: ¿qué ventaja tiene DataStore sobre SharedPreferences?
          ⏰ ¿Se acabó el tiempo (después de las 8:15 PM)? Entonces debes entregar LOS 5 CASOS completos (los 5 retos con sus capturas y códigos) en tu PDF.
          🤖 Prompts de IA para los 5 retos (qué pedirle a la IA en cada caso — clic para ver)
          🌓 Reto 1 — Modo oscuro:
          Soy estudiante de Desarrollo Móvil I (Android con Kotlin en Android Studio). Mi reto: guardar modo oscuro (Switch) y carné en SharedPreferences con apply(). Actúa como mi tutor: 1) explícame apply() vs commit() con un ejemplo corto, 2) revisa el código que te pegue y dime qué está mal y por qué, 3) dame pistas antes de darme la solución completa. Sin SQLite, solo SharedPreferences.
          🔐 Reto 2 — Login:
          Soy estudiante de Desarrollo Móvil I (Android con Kotlin en Android Studio). Mi reto: un login sencillo que recuerda la sesión con SharedPreferences (putBoolean activa + putString user + apply(), y clear() para salir). Actúa como mi tutor: 1) explícame dónde leer la sesión para saltar el login, 2) revisa el código que te pegue y dime qué está mal y por qué, 3) dame pistas antes de la solución completa. Sin SQLite, solo SharedPreferences.
          📝 Reto 3 — Nota en archivo:
          Soy estudiante de Desarrollo Móvil I (Android con Kotlin en Android Studio). Mi reto: un bloc de notas que guarda en nota.txt dentro de filesDir usando openFileOutput/openFileInput con .use { }. Actúa como mi tutor: 1) explícame qué garantiza .use { } y qué es el sandbox por UID, 2) revisa el código que te pegue y dime qué está mal y por qué, 3) dame pistas antes de la solución completa. Sin SQLite, solo archivos internos.
          👤 Reto 4 — Perfil con foto:
          Soy estudiante de Desarrollo Móvil I (Android con Kotlin en Android Studio). Mi reto: un perfil que guarda el nombre en SharedPreferences y la foto como foto_perfil.jpg en filesDir, mostrándola con Coil. Actúa como mi tutor: 1) explícame compress() a JPG y cómo cargar el archivo con Coil sin bloquear la UI, 2) revisa el código que te pegue y dime qué está mal y por qué, 3) dame pistas antes de la solución completa. Sin SQLite.
          ❤️ Reto 5 — Favoritos DataStore:
          Soy estudiante de Desarrollo Móvil I (Android con Kotlin en Android Studio). Mi reto: 3 favoritos on/off guardados en Preferences DataStore con booleanPreferencesKey, edit() suspendido y lectura con Flow + lifecycleScope. Actúa como mi tutor: 1) explícame la ventaja de DataStore sobre SharedPreferences con un ejemplo corto, 2) revisa el código que te pegue y dime qué está mal y por qué, 3) dame pistas antes de la solución completa. Sin SQLite.

          ¡FELICIDADES POR COMPLETAR LA PRÁCTICA A1!

          Exporta tu informe con la captura de la evaluación (≥25/40), la captura de tu ruleta y el código de tu reto asignado en PDF y súbelo a la plataforma institucional. Recuerda: en clase entregas SOLO tu reto; fuera de tiempo, los 5 casos.