Programación de Dispositivos Móviles

Temas Avanzados de Android

Guía visual y pedagógica para clase. Cubre almacenamiento, mapas, sensores, multimedia, widgets, servicios web y Bluetooth — todo lo que necesitas para construir apps Android de nivel profesional.

🗃️ Datos 🗺️ Mapas 📡 Sensores 🎬 Multimedia 🧩 Widgets 🌐 Servicios Web 📶 Bluetooth
01
Datos y Base de Datos
SharedPreferences · DataStore · Room · Firebase

🤔 ¿Por qué necesitamos almacenar datos?

Imagina una app de notas que pierde todo al cerrarla, o un juego que olvida tu puntuación. Toda app útil necesita persistir datos. Android ofrece varias opciones según la complejidad y ubicación de los datos:

🔑
SharedPreferences
Pares clave-valor. Ideal para preferencias del usuario: tema, idioma, sesión activa.
DataStore
La versión moderna de SharedPreferences. Usa corrutinas. Recomendado para apps nuevas.
🗄️
Room (SQLite)
Base de datos relacional local. Para listas, historial, cachés offline.
☁️
Firebase
Base de datos en la nube. Sincronización en tiempo real entre dispositivos.
💡 Regla general

¿Datos simples de configuración? → DataStore. ¿Listas o datos estructurados? → Room. ¿Varios usuarios o tiempo real? → Firebase.

📦 SharedPreferences vs DataStore

Ambos guardan pares clave → valor, pero con diferencias importantes:

SharedPreferences (clásico)

  • API antigua, síncrona
  • commit() bloquea el hilo
  • apply() es asíncrono pero sin confirmación
  • No apto para operaciones complejas
  • Todavía válido para casos simples

DataStore ✅ Recomendado

  • Basado en corrutinas y Flow
  • Operaciones siempre asíncronas y seguras
  • Manejo de errores con Result
  • Preferences DataStore o Proto DataStore
  • Reemplaza SharedPreferences
// SharedPreferences — forma clásica
val prefs = getSharedPreferences("mi_app", Context.MODE_PRIVATE)
prefs.edit().apply {
    putString("nombre", "Juan")
    putBoolean("sesion", true)
}.apply()  // asíncrono

// DataStore — forma moderna (recomendada)
val NAME_KEY = stringPreferencesKey("nombre")

// Guardar
lifecycleScope.launch {
    dataStore.edit { prefs -> prefs[NAME_KEY] = "Juan" }
}

// Leer como Flow reactivo
val nombre: Flow<String> = dataStore.data
    .map { prefs -> prefs[NAME_KEY] ?: "" }
⚠️ Error frecuente

Usar commit() en el hilo principal bloquea la UI y puede causar ANR (App Not Responding). Usar siempre apply() o DataStore con corrutinas.

🗄️ Room Database: el ORM oficial de Android

Room es la capa de abstracción sobre SQLite que Google recomienda oficialmente. En lugar de escribir SQL directamente, usas anotaciones Kotlin y Room genera el código SQL por ti.

Analogía: Room es como un archivero automático. Tú le dices qué datos guardar (@Entity), qué operaciones hacer (@Dao) y él gestiona el archivador (SQLite) por ti.

💡 Los 4 componentes de Room

@Entity → Una clase Kotlin que representa una tabla.
@Dao → Interface con los métodos de acceso (SELECT, INSERT, DELETE).
@Database → La clase que gestiona la BD, es el punto de entrada.
Repository → Patrón de diseño para abstraer el origen de datos (no es de Room).

Arquitectura Room

UI / ViewModel — observa Flow/LiveData
Repository — capa de abstracción
DAO — @Query @Insert @Delete
Room Database — @Database
SQLite — motor de BD
// 1. ENTITY — representa una tabla
@Entity(tableName = "usuarios")
data class Usuario(
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0,
    val nombre: String,
    val email: String
)

// 2. DAO — operaciones sobre la tabla
@Dao
interface UsuarioDao {
    @Query("SELECT * FROM usuarios")
    fun getAll(): Flow<List<Usuario>>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(u: Usuario)

    @Delete
    suspend fun delete(u: Usuario)
}
⚠️ Error frecuente

Acceder a la base de datos Room desde el hilo principal causa una excepción. Siempre usa suspend fun en el DAO y llámalo desde una corrutina (viewModelScope.launch).

🙋 Pregunta para el alumno

Si tienes una app de lista de la compra, ¿qué sería la @Entity? ¿Qué métodos necesitarías en el @Dao?

☁️ Firebase: base de datos en la nube

Firebase es la plataforma de Google para apps en la nube. Para bases de datos ofrece dos opciones:

🔴 Realtime Database

  • Árbol JSON en tiempo real
  • Sincronización offline automática
  • Ideal para: chats, presencia, datos colaborativos
  • Estructura simple pero rígida

🔵 Cloud Firestore ✅

  • Colecciones y documentos (como MongoDB)
  • Consultas avanzadas con filtros y ordenación
  • Escalabilidad superior
  • Ideal para apps de producción
📱 Ejemplo aplicado

Una app de chat usa Realtime Database para que los mensajes aparezcan al instante en todos los dispositivos. Una app de e-commerce usa Firestore para consultar productos por categoría y precio.

📋 Mini resumen de la sección

Almacenamiento simple: DataStore. BD local: Room (ORM sobre SQLite, con @Entity + @Dao + @Database). Nube: Firebase Realtime (tiempo real y chats) o Firestore (apps complejas). Patrón: ViewModel → Repository → DAO → Room/Firebase.

02
Mapas y GPS
Google Maps SDK · FusedLocationProvider · Geofencing

🗺️ ¿Qué podemos hacer con mapas en Android?

Android permite integrar mapas interactivos completos en cualquier app gracias al Google Maps SDK. Para usarlo necesitas una API Key gratuita de Google Cloud Console.

📍
Marcadores (Markers)
Pines en el mapa con título, descripción e icono personalizado.
🛣️
Polilíneas
Líneas que trazan rutas y caminos sobre el mapa.
Polígonos y Círculos
Delimitar áreas geográficas o mostrar radios de acción.
🛰️
Tipos de mapa
Normal, Satélite, Terreno o Híbrido.
// Implementación básica — abrir mapa y añadir marcador
class MapActivity : AppCompatActivity(), OnMapReadyCallback {

    override fun onMapReady(map: GoogleMap) {
        val madrid = LatLng(40.4168, -3.7038)
        map.addMarker(
            MarkerOptions().position(madrid).title("Madrid")
        )
        map.animateCamera(
            CameraUpdateFactory.newLatLngZoom(madrid, 12f)
        )
    }
}
FuncionalidadMétodoDescripción
MarcadoraddMarker(MarkerOptions())Punto con título e icono
Tipo de mapamapType = MAP_TYPE_*Normal, Satélite, Terreno, Híbrido
Cámara/ZoomCameraUpdateFactory.newLatLngZoom()Mover y enfocar el mapa
PolilíneaaddPolyline(PolylineOptions())Trazar rutas
PolígonoaddPolygon(PolygonOptions())Delimitar áreas
CírculoaddCircle(CircleOptions())Radio alrededor de un punto

📡 Geolocalización: FusedLocationProviderClient

Analogía: Es como el GPS de un coche, pero inteligente. En lugar de usar solo satélites (lento al arrancar), combina GPS + Wi-Fi + redes móviles para darte la posición más rápida y precisa posible con el menor consumo de batería.

💡 Idea clave

FusedLocationProviderClient es la forma recomendada por Google para obtener la ubicación. No uses directamente LocationManager, GpsProvider ni WiFiProvider — FusedLocation lo gestiona todo por ti.

// Obtener la última ubicación conocida
val fusedClient = LocationServices.getFusedLocationProviderClient(this)

fusedClient.lastLocation.addOnSuccessListener { location ->
    location?.let {
        val lat = it.latitude   // ej: 40.4168
        val lng = it.longitude  // ej: -3.7038
    }
}

// Actualizaciones periódicas (cada 5 segundos)
val request = LocationRequest.Builder(
    Priority.PRIORITY_HIGH_ACCURACY, 5000L)
    .setMinUpdateIntervalMillis(2000L)
    .build()

fusedClient.requestLocationUpdates(request, locationCallback,
    Looper.getMainLooper())
⚠️ Error frecuente

Olvidar declarar el permiso ACCESS_FINE_LOCATION en el AndroidManifest y también solicitarlo en runtime. Ambas cosas son necesarias. Sin el permiso runtime, la app fallará en Android 6+.

🔔 Geofencing: zonas con alarma geográfica

Analogía: Es como un detector de presencia pero en el mundo real. Defines un círculo en el mapa (la "valla") y Android te avisa automáticamente cuando el usuario entra o sale de esa zona, incluso con la app cerrada.

📱 Ejemplo aplicado

Una app de delivery activa una notificación al repartidor cuando entra en la zona del restaurante. Una app de control de asistencia registra la entrada/salida automáticamente al llegar al trabajo.

// Crear una geovalla circular de 150 metros
val geofence = Geofence.Builder()
    .setRequestId("oficina")
    .setCircularRegion(lat, lng, 150f)  // radio en metros
    .setTransitionTypes(
        Geofence.GEOFENCE_TRANSITION_ENTER or
        Geofence.GEOFENCE_TRANSITION_EXIT)
    .setExpirationDuration(Geofence.NEVER_EXPIRE)
    .build()
🙋 Pregunta para el alumno

Piensa en 3 apps del día a día que podrían usar Geofencing. ¿Qué zona definiría cada una y qué acción realizaría al entrar/salir?

📋 Mini resumen

Google Maps SDK para mapas visuales. FusedLocationProvider para ubicación precisa y eficiente (GPS+WiFi+red). Geofencing para detectar entrada/salida de zonas geográficas. Siempre pedir permiso ACCESS_FINE_LOCATION en runtime.

03
Sensores
Acelerómetro · Giroscopio · SensorManager · Casos de uso

📡 Tipos de sensores disponibles en Android

El móvil es un laboratorio de sensores. Android los clasifica en cuatro categorías. No todos los dispositivos tienen todos los sensores, por lo que siempre hay que comprobar su disponibilidad.

CategoríaSensoresAplicaciones típicas
🏃 MovimientoAcelerómetro, Giroscopio, Gravedad, Rotación linealJuegos, gestos, orientación, step counter
🧭 PosiciónMagnetómetro (brújula), ProximidadBrújula digital, apagar pantalla en llamadas
🌡️ EntornoLuz ambiental, Barómetro, Temperatura, HumedadBrillo automático, altitud, predicción del tiempo
❤️ BiométricoRitmo cardíaco, SpO2, Huella dactilarSalud, fitness, autenticación

⚙️ Cómo acceder a los sensores: el patrón SensorManager

Analogía: SensorManager es como el controlador de una placa de sensores. Le dices qué sensor quieres escuchar, a qué velocidad, y él te llama cuando hay nuevos datos.

El flujo siempre es el mismo: obtener el SensorManager → obtener el sensor → registrar un listener → recibir datos en onSensorChangeddesregistrar cuando no se necesite.

class SensorActivity : AppCompatActivity(), SensorEventListener {

    private lateinit var sensorManager: SensorManager
    private var sensor: Sensor? = null

    override fun onCreate(b: Bundle?) {
        super.onCreate(b)
        sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
        sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
    }

    override fun onResume() {          // Registrar al volver a primer plano
        super.onResume()
        sensorManager.registerListener(this, sensor,
            SensorManager.SENSOR_DELAY_NORMAL)
    }

    override fun onPause() {           // SIEMPRE desregistrar al pausar
        super.onPause()
        sensorManager.unregisterListener(this)
    }

    override fun onSensorChanged(event: SensorEvent) {
        val x = event.values[0]  // Eje X
        val y = event.values[1]  // Eje Y
        val z = event.values[2]  // Eje Z
    }
}
⚠️ Error frecuente crítico

Olvidar unregisterListener(this) en onPause(). Si el sensor sigue activo en segundo plano, consume batería constantemente aunque el usuario no esté usando la app.

🏎️ Velocidades de muestreo (SENSOR_DELAY)

ConstanteAprox. (µs)Uso recomendado
SENSOR_DELAY_NORMAL200.000Cambios de orientación y UI
SENSOR_DELAY_UI60.000Actualizaciones de interfaz
SENSOR_DELAY_GAME20.000Juegos y realidad aumentada
SENSOR_DELAY_FASTEST0 (máxima)Análisis en tiempo real
💡 Idea clave

Cuanto más rápido el muestreo, más batería consume. Usa la velocidad mínima que satisfaga las necesidades de tu app. Para un contador de pasos, NORMAL es más que suficiente.

🛠️ Casos de uso prácticos

📳
Detector de agitación (Shake)
Calcular la magnitud del vector: √(x²+y²+z²). Si supera un umbral (~15 m/s²) → agitación detectada.
👣
Contador de pasos
TYPE_STEP_COUNTER acumula pasos desde el reinicio. TYPE_STEP_DETECTOR dispara un evento por cada paso.
🧭
Brújula digital
TYPE_MAGNETIC_FIELD + getOrientation() → azimuth (ángulo respecto al Norte).
🔄
Orientación pantalla
TYPE_ROTATION_VECTOR devuelve un cuaternión que se convierte en matriz de rotación.
🙋 Pregunta para el alumno

¿En qué ciclo de vida debes registrar el listener del sensor? ¿Por qué es peligroso registrarlo en onCreate() y no desregistrarlo en onPause()?

📋 Mini resumen

SensorManager obtiene el sensor. registerListener en onResume(). unregisterListener en onPause(). onSensorChanged recibe los datos en values[0/1/2]. Velocidad de muestreo según necesidad.

04
Multimedia
MediaPlayer · ExoPlayer/Media3 · CameraX · Permisos

🎵 MediaPlayer: reproducción de audio básica

MediaPlayer permite reproducir audio desde recursos locales (carpeta res/raw/) o desde URLs remotas. Requiere gestión cuidadosa del ciclo de vida.

💡 Analogía

MediaPlayer es como un reproductor de casete: simple, hace lo básico, pero hay que rebobinarlo manualmente, liberarlo cuando no se usa y gestionar cada estado.

// Desde recurso local (res/raw/musica.mp3)
val player = MediaPlayer.create(this, R.raw.musica)
player?.start()

// Desde URL — no bloqueante
MediaPlayer().apply {
    setDataSource("https://ejemplo.com/audio.mp3")
    setOnPreparedListener { start() }
    prepareAsync()  // No bloquea el hilo principal
}

// IMPORTANTE: siempre liberar en onDestroy()
override fun onDestroy() {
    super.onDestroy()
    player?.release()
    player = null
}
⚠️ Error frecuente

No llamar a release() en onDestroy() provoca fugas de memoria (memory leak). El MediaPlayer sigue ocupando recursos aunque la pantalla se cierre.

🎬 ExoPlayer (Media3): vídeo y streaming profesional

ExoPlayer, ahora incluido en AndroidX Media3, es el reproductor de vídeo de referencia en Android. Lo usan YouTube, Spotify y la mayoría de grandes apps.

MediaPlayer

  • Solo formatos básicos (MP3, MP4, WAV)
  • Sin soporte HLS/DASH nativo
  • Difícil de personalizar
  • OK para audio simple

ExoPlayer / Media3 ✅

  • HLS, DASH, SmoothStreaming
  • PlayerView con controles integrados
  • Altamente personalizable
  • Recomendado para vídeo
// ExoPlayer con Media3
private var player: ExoPlayer? = null

override fun onStart() {
    super.onStart()
    player = ExoPlayer.Builder(this).build().also {
        binding.playerView.player = it
        val item = MediaItem.fromUri("https://ejemplo.com/video.m3u8")
        it.setMediaItem(item)
        it.prepare()
        it.playWhenReady = true
    }
}

override fun onStop() {
    super.onStop()
    player?.release()
    player = null
}

📷 CameraX: la cámara moderna en Android

CameraX es la API de cámara recomendada por Google. Proporciona una interfaz consistente en todos los dispositivos y versiones de Android, eliminando la fragmentación del API de cámara anterior.

👁️
Preview
Vista previa en tiempo real. Lo que se ve en pantalla antes de capturar.
📸
ImageCapture
Capturar fotografías. Guardarlas en un archivo o procesarlas en memoria.
🎥
VideoCapture
Grabar vídeo con audio. Guardar el fichero resultante.
🔍
ImageAnalysis
Analizar cada frame en tiempo real. Para IA, detección de objetos, QR...
💡 Ciclo de vida vinculado

CameraX se vincula automáticamente al ciclo de vida de la Activity o Fragment con bindToLifecycle(). Cuando la app va a segundo plano, la cámara se libera sola. No hay que gestionar el ciclo manualmente.

🔐 Permisos de cámara y micrófono

CAMERA y RECORD_AUDIO son permisos peligrosos. Deben declararse en el Manifest y solicitarse en runtime. Sin el permiso en runtime, la app fallará con una excepción en Android 6+.

// Solicitar permisos en runtime
val requestPermissions = registerForActivityResult(
    ActivityResultContracts.RequestMultiplePermissions()) { perms ->
    val camOk = perms[Manifest.permission.CAMERA] == true
    val micOk = perms[Manifest.permission.RECORD_AUDIO] == true
    if (camOk && micOk) startCamera()
}

requestPermissions.launch(arrayOf(
    Manifest.permission.CAMERA,
    Manifest.permission.RECORD_AUDIO
))
🙋 Pregunta para el alumno

¿Qué pasa si el usuario deniega el permiso de cámara? ¿Cómo debería responder la app? ¿Debe bloquearse o puede funcionar parcialmente?

📋 Mini resumen

MediaPlayer para audio básico (liberar en onDestroy). ExoPlayer/Media3 para vídeo y streaming HLS/DASH. CameraX con 4 use cases (Preview, Capture, Video, Analysis). Siempre solicitar permisos CAMERA y RECORD_AUDIO en runtime.

05
Widgets
AppWidgetProvider · RemoteViews · Actualización periódica

🧩 ¿Qué es un AppWidget?

Un AppWidget es una mini-vista de tu app que se muestra directamente en el escritorio del móvil. Piensa en el widget del tiempo, del reloj o de Spotify: muestran información sin que el usuario tenga que abrir la app.

💡 RemoteViews: la clave de los widgets

Los widgets no usan Views normales. Usan RemoteViews, que es una descripción de una vista que se ejecuta en el proceso del lanzador del escritorio (no en el proceso de tu app). Por eso los controles disponibles son limitados: solo TextView, ImageView, Button, ProgressBar, y colecciones (ListView, GridView).

Los 4 componentes que necesita cualquier widget:

ComponenteDescripción
Layout XMLEl diseño visual del widget (solo Views compatibles con RemoteViews)
Widget Info XMLConfiguración: tamaño mínimo, periodo de actualización, preview
AppWidgetProviderBroadcastReceiver que recibe eventos del widget (onUpdate, onEnabled…)
AndroidManifestRegistro del receptor con el filtro ACTION_APPWIDGET_UPDATE

⚙️ AppWidgetProvider y RemoteViews

// res/xml/widget_info.xml — configuración del widget
<appwidget-provider
    android:minWidth="110dp"
    android:minHeight="40dp"
    android:updatePeriodMillis="1800000"   // 30 minutos (mínimo)
    android:previewImage="@drawable/widget_preview"
    android:initialLayout="@layout/widget_layout"/>

// AppWidgetProvider — lógica del widget
class MiWidget : AppWidgetProvider() {
    override fun onUpdate(context: Context, manager: AppWidgetManager, ids: IntArray) {
        ids.forEach { id ->
            val views = RemoteViews(context.packageName, R.layout.widget_layout)
            views.setTextViewText(R.id.tv_widget, "¡Actualizado!")

            // Acción al tocar el widget
            val intent = Intent(context, MainActivity::class.java)
            val pi = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
            views.setOnClickPendingIntent(R.id.tv_widget, pi)
            manager.updateAppWidget(id, views)
        }
    }
}
⚠️ Error frecuente

El updatePeriodMillis mínimo es 30 minutos (1.800.000 ms). Si necesitas actualizaciones más frecuentes, usa WorkManager o AlarmManager. Android ignorará períodos inferiores a 30 minutos.

✅ Mejores prácticas
  • Mantener el código de onUpdate() ligero y rápido
  • Para listas en widgets: usar RemoteViewsService + RemoteViewsFactory
  • El widget vive en un proceso separado → no acceder a variables de la Activity
  • Para actualización frecuente → WorkManager que llama al widgetManager
🙋 Pregunta para el alumno

¿Por qué no puedes usar cualquier View en un widget? ¿Qué tiene de especial RemoteViews comparado con una View normal?

📋 Mini resumen

AppWidget = mini-app en el escritorio. Requiere Layout XML + Widget Info XML + AppWidgetProvider (BroadcastReceiver) + Manifest. RemoteViews limita las vistas disponibles. Actualización mínima automática: 30 min. Para más frecuencia: WorkManager.

06
Servicios Web
Retrofit · OkHttp · REST · JWT · Corrutinas

🌐 ¿Qué es una API REST y por qué la necesito?

La mayoría de apps modernas obtienen datos de internet: el tiempo, las noticias, el perfil del usuario... Todo eso viene de un servidor web a través de una API REST.

💡 Analogía

Una API REST es como un camarero de restaurante: tú haces una petición (GET, POST, PUT, DELETE), él va a la cocina (servidor) y te trae la respuesta (JSON). No necesitas saber cómo funciona la cocina, solo qué pedir y cómo interpretarlo.

El stack recomendado para Android:

ViewModel — llama al Repository, expone StateFlow a la UI
Repository — abstrae la fuente de datos (red, caché local)
Retrofit — cliente HTTP, define endpoints con anotaciones
OkHttp — capa de red, interceptores, logs, timeouts
Gson / Moshi — convierte JSON ↔ objetos Kotlin

⚙️ Retrofit: definición de endpoints con anotaciones

Con Retrofit defines una interface Kotlin donde cada función representa un endpoint. Retrofit genera automáticamente el código de red.

// Interface de la API — describe los endpoints
interface ApiService {
    @GET("users")
    suspend fun getUsers(): Response<List<User>>

    @GET("users/{id}")
    suspend fun getUser(@Path("id") id: Int): Response<User>

    @POST("users")
    suspend fun createUser(@Body user: UserRequest): Response<User>

    @GET("products")
    suspend fun searchProducts(
        @Query("q") query: String,
        @Query("page") page: Int = 1
    ): Response<List<Product>>
}

// Uso en ViewModel con corrutinas
fun loadUsers() {
    viewModelScope.launch {
        try {
            val response = RetrofitClient.instance.getUsers()
            if (response.isSuccessful) {
                _users.value = response.body() ?: emptyList()
            }
        } catch (e: IOException) {
            Log.e("VM", "Sin conexión: ${e.message}")
        }
    }
}

🔐 Autenticación con JWT

JWT (JSON Web Token) es el estándar para autenticar peticiones a APIs. Funciona así: el usuario hace login → el servidor devuelve un token → la app adjunta ese token a todas las peticiones siguientes.

💡 Analogía

JWT es como una pulsera de acceso en un festival. Te la dan al entrar (login), y la muestras en cada zona para demostrar que tienes permiso. No tienes que volver a la entrada cada vez.

// Interceptor OkHttp — añade el token a todas las peticiones
class AuthInterceptor(private val tokenProvider: () -> String?) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val token = tokenProvider()
        return if (token != null) {
            val request = chain.request().newBuilder()
                .header("Authorization", "Bearer $token")
                .build()
            chain.proceed(request)
        } else {
            chain.proceed(chain.request())
        }
    }
}
⚠️ Error frecuente

Nunca guardes el token JWT en SharedPreferences sin cifrar. Usa EncryptedSharedPreferences o el Android Keystore para guardar datos sensibles de forma segura.

🙋 Pregunta para el alumno

¿Por qué las llamadas a la API deben hacerse desde el ViewModel y no directamente desde la Activity? ¿Qué pasaría si giras el teléfono y la petición estaba en curso?

📋 Mini resumen

Retrofit + OkHttp + Gson = stack de red estándar Android. La interface define endpoints con @GET/@POST/@Path/@Query. Usar suspend fun + corrutinas para no bloquear la UI. JWT con interceptor OkHttp. Patrón: ViewModel → Repository → ApiService.

07
Bluetooth
BT Clásico · BLE · GATT · Sensores IoT

📶 Bluetooth Clásico vs BLE

Bluetooth existe en dos variantes con casos de uso muy diferentes. Elegir la correcta es fundamental para el consumo de batería y el tipo de comunicación:

CaracterísticaBluetooth ClásicoBLE (Low Energy)
Velocidad datosAlta (hasta 3 Mbps)Baja (~1 Mbps)
Consumo energíaAltoMuy bajo (años con pila de botón)
RangoHasta 100 metrosHasta 100 metros
Latencia conexiónAlta (~2 seg)Muy baja (~3 ms)
Casos de usoAudio, transferencia de archivosIoT, wearables, sensores
ArquitecturaPerfiles (A2DP, SPP, HFP...)GATT (Servicios + Características)
💡 Regla de elección

¿Necesitas transmitir audio o archivos grandes? → BT Clásico. ¿Conectar a un sensor de temperatura, pulsera o baliza? → BLE. La batería importa más que la velocidad para IoT.

🏗️ Arquitectura GATT de BLE

GATT (Generic Attribute Profile) es cómo BLE organiza los datos. Funciona como un sistema de archivos jerárquico:

Dispositivo BLE
Servidor GATT
Servicio (UUID)
Ej: Servicio de temperatura 0x1809
Característica (UUID)
Valor legible/escribible/notificable
// 1. Escanear dispositivos BLE cercanos
val scanCallback = object : ScanCallback() {
    override fun onScanResult(callbackType: Int, result: ScanResult) {
        val device = result.device
        Log.d("BLE", "Encontrado: ${device.name} / ${device.address}")
        // RSSI = intensidad de señal en dBm (más cercano a 0 = más fuerte)
        Log.d("BLE", "Señal: ${result.rssi} dBm")
    }
}
scanner.startScan(filters, settings, scanCallback)

// 2. Conectar y leer una característica
val gatt = device.connectGatt(context, false,
    object : BluetoothGattCallback() {
        override fun onConnectionStateChange(gatt, status, newState) {
            if (newState == BluetoothProfile.STATE_CONNECTED)
                gatt.discoverServices()
        }
        override fun onCharacteristicRead(
            gatt, characteristic, value: ByteArray, status) {
            val datos = String(value)
            runOnUiThread { actualizarUI(datos) }
        }
    }
)

// 3. Siempre cerrar al terminar
gatt?.close()
⚠️ Error frecuente — permisos BLE (API 31+)

Desde Android 12 (API 31), los permisos de Bluetooth cambiaron completamente. Necesitas BLUETOOTH_SCAN, BLUETOOTH_CONNECT (y BLUETOOTH_ADVERTISE si emites). Para versiones anteriores: BLUETOOTH, BLUETOOTH_ADMIN y ACCESS_FINE_LOCATION.

📱 Ejemplo aplicado

Una app de salud se conecta por BLE a una pulsera inteligente para leer la frecuencia cardíaca cada 5 segundos. La pulsera actúa como servidor GATT con un servicio de frecuencia cardíaca (UUID 0x180D) y una característica (UUID 0x2A37) que notifica automáticamente al móvil.

🙋 Pregunta para el alumno

¿Por qué es importante llamar a scanner.stopScan()? ¿Qué consecuencias tiene dejarlo activo continuamente?

📋 Mini resumen

BT Clásico para audio/archivos. BLE para IoT/sensores con muy bajo consumo. GATT organiza los datos en Servicios → Características con UUIDs. Flujo: escanear → conectar → discoverServices → leer/suscribirse → cerrar. Permisos cambiaron en API 31+.

🏋️
Actividades Prácticas
7 ejercicios para consolidar el aprendizaje

Estos ejercicios pueden usarse como práctica de aula, tarea o proyecto. Están ordenados de menor a mayor complejidad.

1
Room + ViewModel
CRUD completo con Room, ViewModel y Flow. Mostrar lista con RecyclerView. Añadir, editar y eliminar contactos.

Conceptos: @Entity, @Dao, Repository, StateFlow, RecyclerView
2
Mapa + Ubicación
Mostrar mapa con Google Maps, obtener la ubicación actual y añadir un marcador. Dibujar la ruta como polilínea.

Conceptos: Google Maps SDK, FusedLocationProvider, PolylineOptions
3
Detector de agitación
Implementar Shake Detector con SensorEventListener. Vibrar al detectar. Añadir podómetro con TYPE_STEP_COUNTER.

Conceptos: SensorManager, magnitud del vector, umbral
4
Reproductor de vídeo
Integrar ExoPlayer con URL de streaming HLS. Controles play/pause/seek y barra de progreso.

Conceptos: ExoPlayer, Media3, PlayerView, MediaItem
5
Widget del tiempo
AppWidget que muestra temperatura y descripción del tiempo. Actualizar cada 30 minutos con WorkManager + API meteorológica.

Conceptos: AppWidgetProvider, RemoteViews, WorkManager
6
Consulta REST + Room
Consumir JSONPlaceholder con Retrofit. Almacenar en Room (offline-first). Mostrar lista aunque no haya internet.

Conceptos: Retrofit, Repository, Room, offline-first
7
BLE Scanner
Escanear dispositivos BLE cercanos y mostrarlos con nombre, dirección MAC y RSSI. Conectar a uno y leer una característica.

Conceptos: BluetoothLeScanner, GATT, ScanCallback

💬 Actividades de reflexión para clase

📝 Explica con tus palabras
¿En qué se diferencia Room de SQLite nativo? ¿Por qué Room es "mejor"?
🔗 Relaciona con una app
¿Qué app real conoces que use Geofencing? ¿Y BLE? ¿Y streaming con ExoPlayer?
🔍 Detecta el error
Un alumno pone el registerListener en onCreate() y no llama a unregisterListener. ¿Qué pasará? ¿Cómo lo corriges?
🏗️ Completa el esquema
Dibuja el flujo completo de una petición Retrofit: desde el botón en la UI hasta el JSON del servidor y de vuelta.
📖
Glosario de términos
Definiciones claras de todos los términos técnicos del tema
ORM (Object Relational Mapper)
Herramienta que convierte objetos de código (clases Kotlin) en filas de una base de datos y viceversa, sin escribir SQL manualmente. Room es el ORM de Android.
@Entity
Anotación de Room que marca una clase Kotlin como una tabla de la base de datos. Cada instancia de la clase = una fila en la tabla.
@Dao (Data Access Object)
Interface anotada de Room donde se definen los métodos para acceder a la BD: SELECT, INSERT, UPDATE, DELETE, con sus anotaciones correspondientes.
Flow (Kotlin)
Stream asíncrono de datos reactivo. Cuando los datos cambian, los observadores se actualizan automáticamente. Room lo usa para que la UI se actualice sola al cambiar la BD.
Corrutina
Forma de ejecutar código asíncrono sin bloquear el hilo principal. En Android se usan para llamadas a red, BD y operaciones lentas. suspend fun marca una función que puede pausarse.
suspend fun
Función de Kotlin que puede suspenderse (pausarse) sin bloquear el hilo. Solo puede llamarse desde otra suspend fun o desde una corrutina.
StateFlow / LiveData
Contenedor observable de un valor. Cuando el valor cambia, todos los observadores (como la UI) se notifican automáticamente. StateFlow es la versión moderna.
DataStore
Biblioteca Jetpack para almacenar pares clave-valor o datos tipados de forma asíncrona y segura, usando corrutinas. Reemplaza SharedPreferences.
Firebase Realtime Database
BD NoSQL en la nube de Google en formato árbol JSON. Los cambios se sincronizan en tiempo real entre todos los clientes conectados.
Cloud Firestore
BD documental escalable de Firebase. Organiza los datos en Colecciones → Documentos → Campos. Más potente y escalable que Realtime Database.
FusedLocationProviderClient
API de Google que combina GPS, Wi-Fi y datos de red para dar la mejor ubicación posible con el menor consumo de batería. Es la API de ubicación recomendada en Android.
Geofencing
Funcionalidad que permite definir zonas geográficas circulares y recibir alertas cuando el dispositivo entra o sale de ellas, incluso con la app cerrada.
SensorManager
Servicio del sistema Android para acceder a los sensores físicos del dispositivo. Actúa como intermediario entre el hardware y la aplicación.
SensorEventListener
Interface que implementa tu Activity o clase para recibir datos de los sensores. Tiene dos métodos: onSensorChanged() y onAccuracyChanged().
SENSOR_DELAY
Velocidad de muestreo del sensor. NORMAL (200ms), UI (60ms), GAME (20ms), FASTEST (máxima). Mayor frecuencia = más batería consumida.
ExoPlayer / Media3
Reproductor multimedia de Google para Android que soporta streaming HLS/DASH y es altamente personalizable. Ahora integrado en AndroidX Media3.
HLS (HTTP Live Streaming)
Protocolo de streaming de Apple adoptado universalmente. Divide el vídeo en fragmentos pequeños y los sirve sobre HTTP. Usado por YouTube, Netflix, Twitch.
CameraX
Biblioteca AndroidX para acceder a la cámara de forma unificada en todos los dispositivos. Ofrece 4 casos de uso: Preview, ImageCapture, VideoCapture, ImageAnalysis.
ProcessCameraProvider
Punto de entrada de CameraX. Gestiona el ciclo de vida de la cámara vinculándolo al lifecycle de la Activity o Fragment automáticamente.
AppWidgetProvider
Clase especial que extiende BroadcastReceiver para recibir eventos del widget (actualización, habilitación, deshabilitación, borrado).
RemoteViews
Clase que describe una jerarquía de vistas que puede inflarse en el proceso del lanzador del escritorio (fuera de tu app). Necesaria para widgets.
Retrofit
Librería de Square que convierte una interface Kotlin en un cliente HTTP. Con anotaciones (@GET, @POST, @Body...) define los endpoints de una API REST.
OkHttp
Capa de red usada por Retrofit. Gestiona conexiones, timeouts, caché y permite añadir interceptores para logs, autenticación, etc.
JWT (JSON Web Token)
Estándar para autenticación en APIs REST. Token firmado que el servidor emite al hacer login y que el cliente adjunta en cada petición posterior en la cabecera Authorization.
Interceptor (OkHttp)
Componente que se ejecuta antes y/o después de cada petición HTTP. Se usa para añadir cabeceras (token JWT), registrar logs, gestionar errores globales, etc.
BLE (Bluetooth Low Energy)
Variante de Bluetooth diseñada para dispositivos IoT y sensores con muy bajo consumo energético. Usa la arquitectura GATT para organizar los datos.
GATT (Generic Attribute Profile)
Protocolo que define cómo los dispositivos BLE intercambian datos. Organiza la información en Servicios que contienen Características, cada uno identificado por un UUID.
UUID (Universal Unique Identifier)
Identificador único de 128 bits que identifica un servicio o característica BLE. Los más comunes están estandarizados (ej: frecuencia cardíaca = 0x180D).
RSSI (Received Signal Strength Indicator)
Medida de la intensidad de la señal BLE recibida en dBm. Valores más cercanos a 0 = señal más fuerte = dispositivo más cercano.
Repository (patrón)
Capa de abstracción entre la UI/ViewModel y las fuentes de datos (BD Room, API Retrofit, Firebase). Decide si los datos vienen de la red o de la caché local.
ViewModel
Componente de arquitectura Android que sobrevive a los cambios de configuración (girar el teléfono). Contiene la lógica de presentación y expone datos a la UI vía Flow/LiveData.
Permiso peligroso (Dangerous Permission)
Permiso que da acceso a datos sensibles del usuario (cámara, micrófono, ubicación...). Debe declararse en el Manifest Y solicitarse al usuario en runtime (Android 6+).
Resumen del Tema 3
7 bloques que convierten Android en una plataforma profesional

Al dominar estos siete bloques, eres capaz de construir aplicaciones Android completas y de nivel profesional: con datos persistentes, mapas, sensores, multimedia, widgets, comunicación con servidores y dispositivos Bluetooth.

01
Datos y Base de Datos
DataStore para preferencias. Room (ORM sobre SQLite) para datos locales estructurados. Firebase para sincronización en la nube. Patrón ViewModel → Repository → DAO.
02
Mapas y GPS
Google Maps SDK para mapas visuales. FusedLocationProvider para la ubicación más precisa y eficiente. Geofencing para detectar entradas/salidas de zonas geográficas.
03
Sensores
SensorManager + SensorEventListener. Registrar en onResume, desregistrar en onPause. 4 categorías: movimiento, posición, entorno, biométrico. SENSOR_DELAY según necesidad.
04
Multimedia
MediaPlayer para audio básico. ExoPlayer/Media3 para vídeo y streaming HLS/DASH. CameraX para cámara unificada. Siempre pedir CAMERA y RECORD_AUDIO en runtime.
05
Widgets
AppWidgetProvider (BroadcastReceiver) + RemoteViews para el escritorio. Configuración en XML. Actualización mínima automática: 30 minutos. Más frecuencia → WorkManager.
06
Servicios Web
Retrofit + OkHttp + Gson para APIs REST. Interface con @GET/@POST/@Body. suspend fun + corrutinas. JWT con Interceptor. Patrón ViewModel → Repository → ApiService.
07
Bluetooth
BT Clásico para audio y archivos. BLE para IoT con muy bajo consumo. GATT organiza Servicios → Características con UUID. Permisos BLUETOOTH_SCAN y BLUETOOTH_CONNECT (API 31+).
Conclusión del tema

Android es mucho más que una plataforma de formularios y listas. Con los conocimientos de este tema puedes construir apps que se conectan al mundo físico (sensores, GPS, Bluetooth), gestionan datos local y en la nube, y se comunican con servicios externos. Eso es desarrollo móvil profesional.

🔗 Recursos oficiales

📚 Room Database
developer.android.com/training/data-storage/room — Guía oficial completa con ejemplos.
🗺️ Google Maps SDK
developers.google.com/maps/documentation/android-sdk — Referencia del SDK de mapas.
📡 Sensors Overview
developer.android.com/guide/topics/sensors — Referencia completa de sensores Android.
🎬 Media3/ExoPlayer
developer.android.com/guide/topics/media/media3 — Documentación oficial de Media3.
🌐 Retrofit
square.github.io/retrofit — Documentación de la librería HTTP Retrofit.
📶 Bluetooth
developer.android.com/guide/topics/connectivity/bluetooth — Guía BLE y BT clásico.