Tema 1D. Intents, permisos y entorno Android

  • Bloque: B1 — Fundamentos: Kotlin, Compose y entorno Android
  • Duración aproximada: 4 horas
  • RA1 — Aplica tecnologías de desarrollo para dispositivos móviles evaluando sus características y capacidades.
Código Criterio
RA1-a Se han analizado las limitaciones que plantea la ejecución de aplicaciones en los dispositivos móviles.
RA1-c Se han instalado, configurado y utilizado entornos de trabajo para el desarrollo de aplicaciones para dispositivos móviles.
RA1-d Se han identificado configuraciones que clasifican los dispositivos móviles en base a sus características.
RA1-e Se han descrito perfiles que establecen la relación entre el dispositivo y la aplicación.
RA1-f Se ha analizado la estructura de aplicaciones existentes para dispositivos móviles identificando las clases utilizadas.
RA1-g Se han realizado modificaciones sobre aplicaciones existentes.
RA1-h Se han utilizado emuladores para comprobar el funcionamiento de las aplicaciones.

1. El ciclo de vida de una Activity#

Antes de estudiar los Intents, es esencial comprender el ciclo de vida de una Activity, porque determina cuándo se crean, pausan, reanudan y destruyen las pantallas de una app.

Activity creada
   onCreate()       ← inicialización: setContent, configuración
   onStart()        ← la Activity es visible
   onResume()       ← la Activity tiene el foco y el usuario interactúa
   [EN USO]
   onPause()        ← otra Activity toma el foco (ej. diálogo, llamada)
   onStop()         ← la Activity ya no es visible (pasa a background)
   onDestroy()      ← Activity destruida (usuario sale o rotación de pantalla)
   (fin)

Cuándo usar cada callback#

Callback Usar para
onCreate Inicialización única: setContent, cargar datos iniciales
onStart / onStop Iniciar/detener animaciones, sensores de bajo consumo
onResume / onPause Cámara, audio en primer plano, actualizar UI en tiempo real
onDestroy Liberar recursos que no gestionan ciclo de vida automáticamente

En Jetpack Compose, la MainActivity raramente necesita sobrescribir estos callbacks, porque Compose y los ViewModel gestionan el ciclo de vida automáticamente. Sin embargo, es importante conocerlos para entender qué ocurre cuando la app va a background o se rota la pantalla.

 1class MainActivity : ComponentActivity() {
 2    override fun onCreate(savedInstanceState: Bundle?) {
 3        super.onCreate(savedInstanceState)
 4        enableEdgeToEdge()
 5        setContent {
 6            AppFlixTheme {
 7                AppFlixApp()
 8            }
 9        }
10    }
11
12    // En apps con Compose y ViewModel, raramente necesitas sobrescribir onPause/onStop
13    // Los recursos multimedia (cámara, audio) se gestionarán en el Bloque 4
14}

2. Android Manifest#

El AndroidManifest.xml es el archivo de configuración central de la app. Define qué componentes tiene, qué permisos necesita y cómo debe comportarse el sistema.

 1<!-- app/src/main/AndroidManifest.xml -->
 2<?xml version="1.0" encoding="utf-8"?>
 3<manifest xmlns:android="http://schemas.android.com/apk/res/android">
 4
 5    <!-- Permisos que la app necesita (se verá en la sección 4) -->
 6    <uses-permission android:name="android.permission.INTERNET" />
 7
 8    <application
 9        android:name=".AppFlixApplication"
10        android:icon="@mipmap/ic_launcher"
11        android:label="@string/app_name"
12        android:theme="@style/Theme.AppFlix"
13        android:supportsRtl="true">
14
15        <!-- Activity principal — punto de entrada -->
16        <activity
17            android:name=".MainActivity"
18            android:exported="true"
19            android:windowSoftInputMode="adjustResize">
20
21            <!-- Intent filter que la convierte en la activity de inicio -->
22            <intent-filter>
23                <action android:name="android.intent.action.MAIN" />
24                <category android:name="android.intent.category.LAUNCHER" />
25            </intent-filter>
26        </activity>
27
28        <!-- Otras Activities deben declararse aquí también -->
29        <!-- En apps con Compose y Navigation, normalmente solo hay una Activity -->
30
31    </application>
32</manifest>

En arquitecturas modernas con Jetpack Compose y Navigation Compose, es habitual tener una sola Activity y gestionar toda la navegación mediante Composables. Esto simplifica mucho el Manifest. Verás este patrón en el Bloque 2.


3. Intents#

Un Intent es un mensaje que permite a los componentes de Android comunicarse entre sí: activar otra Activity, enviar datos entre pantallas o solicitar una acción al sistema o a otra app.

Hay dos tipos de Intents:

Tipo Descripción Uso
Explícito Especifica exactamente qué componente activar Navegar a otra Activity de la misma app
Implícito Declara una acción; el sistema elige el componente Abrir una URL, compartir contenido, hacer una llamada

3.1 Intent explícito#

 1// En una app con múltiples Activities (patrón menos común con Compose)
 2// Navegar de MainActivity a DetalleActivity pasando un ID
 3
 4// Desde la Activity de origen
 5val intent = Intent(this, DetalleActivity::class.java).apply {
 6    putExtra("PELICULA_ID", 42)
 7    putExtra("PELICULA_TITULO", "Dune: Parte 2")
 8}
 9startActivity(intent)
10
11// En DetalleActivity — recuperar los datos
12class DetalleActivity : ComponentActivity() {
13    override fun onCreate(savedInstanceState: Bundle?) {
14        super.onCreate(savedInstanceState)
15        val id = intent.getIntExtra("PELICULA_ID", -1)
16        val titulo = intent.getStringExtra("PELICULA_TITULO") ?: ""
17
18        setContent {
19            AppFlixTheme {
20                PantallaDetalle(id = id, titulo = titulo)
21            }
22        }
23    }
24}

Arquitectura moderna: En apps con Compose y Navigation Compose (Bloque 2), la navegación entre pantallas se gestiona con NavController.navigate("detalle/$id"), sin necesidad de múltiples Activities. Los Intents explícitos se usan principalmente para integrar componentes externos (cámara del sistema, galería, etc.).

3.2 Intent implícito#

Los Intents implícitos permiten delegar acciones al sistema o a otras apps instaladas:

 1@Composable
 2fun AccionesCompartir(titulo: String, url: String) {
 3    val context = LocalContext.current
 4
 5    Column(
 6        modifier = Modifier.padding(16.dp),
 7        verticalArrangement = Arrangement.spacedBy(8.dp)
 8    ) {
 9        // Compartir texto con cualquier app instalada
10        Button(onClick = {
11            val intent = Intent(Intent.ACTION_SEND).apply {
12                type = "text/plain"
13                putExtra(Intent.EXTRA_SUBJECT, "Te recomiendo esta película")
14                putExtra(Intent.EXTRA_TEXT, "Mira '$titulo': $url")
15            }
16            context.startActivity(
17                Intent.createChooser(intent, "Compartir con...")
18            )
19        }) { Text("Compartir") }
20
21        // Abrir URL en el navegador
22        Button(onClick = {
23            val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
24            // Verificar que hay una app que puede manejar el Intent
25            if (intent.resolveActivity(context.packageManager) != null) {
26                context.startActivity(intent)
27            }
28        }) { Text("Ver en web") }
29
30        // Enviar email
31        Button(onClick = {
32            val intent = Intent(Intent.ACTION_SENDTO).apply {
33                data = Uri.parse("mailto:")
34                putExtra(Intent.EXTRA_EMAIL, arrayOf("soporte@appflix.com"))
35                putExtra(Intent.EXTRA_SUBJECT, "Consulta sobre $titulo")
36            }
37            if (intent.resolveActivity(context.packageManager) != null) {
38                context.startActivity(intent)
39            }
40        }) { Text("Contactar soporte") }
41
42        // Hacer una llamada
43        Button(onClick = {
44            val intent = Intent(Intent.ACTION_DIAL, Uri.parse("tel:+34600000000"))
45            context.startActivity(intent)
46        }) { Text("Llamar al soporte") }
47    }
48}

3.3 Recibir un resultado de otro componente#

Cuando necesitas un resultado de vuelta (ej. una foto tomada con la cámara del sistema, o un archivo seleccionado), usa ActivityResultLauncher:

 1@Composable
 2fun SeleccionarImagen() {
 3    val context = LocalContext.current
 4    var imagenUri by remember { mutableStateOf<Uri?>(null) }
 5
 6    // Registrar el launcher para obtener resultado
 7    val seleccionadorImagen = rememberLauncherForActivityResult(
 8        contract = ActivityResultContracts.GetContent()
 9    ) { uri: Uri? ->
10        imagenUri = uri   // uri es null si el usuario canceló
11    }
12
13    Column(
14        modifier = Modifier.padding(16.dp),
15        horizontalAlignment = Alignment.CenterHorizontally
16    ) {
17        Button(onClick = { seleccionadorImagen.launch("image/*") }) {
18            Text("Seleccionar imagen")
19        }
20
21        imagenUri?.let { uri ->
22            Spacer(modifier = Modifier.height(16.dp))
23            AsyncImage(   // de la librería Coil, se añade en Bloque 3
24                model = uri,
25                contentDescription = "Imagen seleccionada",
26                modifier = Modifier.size(200.dp).clip(RoundedCornerShape(8.dp))
27            )
28        }
29    }
30}

4. Permisos en Android 🔑#

Android protege el acceso a recursos sensibles (cámara, ubicación, contactos, micrófono) mediante un sistema de permisos. El usuario debe conceder estos permisos explícitamente.

4.1 Tipos de permisos#

Tipo Descripción Ejemplo ¿Requiere diálogo?
Normal Bajo riesgo, se conceden automáticamente INTERNET, VIBRATE No
Peligroso Acceso a datos sensibles o hardware CAMERA, READ_CONTACTS, LOCATION Sí, en tiempo de ejecución
Firma Solo para apps del mismo desarrollador READ_CONTACTS del sistema No

Los permisos normales (como INTERNET) solo se declaran en el Manifest y se conceden automáticamente. Los permisos peligrosos requieren que el usuario los apruebe en tiempo de ejecución (desde Android 6.0, API 23).

4.2 Declarar permisos en el Manifest#

Todos los permisos deben declararse en el Manifest, independientemente de si son normales o peligrosos:

 1<manifest>
 2    <!-- Permiso normal (concedido automáticamente) -->
 3    <uses-permission android:name="android.permission.INTERNET" />
 4
 5    <!-- Permisos peligrosos (requieren aprobación del usuario) -->
 6    <uses-permission android:name="android.permission.CAMERA" />
 7    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />  <!-- Android 13+ -->
 8    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
 9        android:maxSdkVersion="32" />  <!-- solo en Android ≤ 12 -->
10
11    <!-- Características hardware opcionales -->
12    <uses-feature android:name="android.hardware.camera" android:required="false" />
13</manifest>

4.3 Solicitar permisos en tiempo de ejecución (Compose)#

 1@Composable
 2fun PantallaConPermisoCamara() {
 3    val context = LocalContext.current
 4    var tienePermiso by remember {
 5        mutableStateOf(
 6            ContextCompat.checkSelfPermission(
 7                context,
 8                Manifest.permission.CAMERA
 9            ) == PackageManager.PERMISSION_GRANTED
10        )
11    }
12    var permisoDenegadoPermanentemente by remember { mutableStateOf(false) }
13
14    // Launcher para solicitar un único permiso
15    val solicitarPermiso = rememberLauncherForActivityResult(
16        contract = ActivityResultContracts.RequestPermission()
17    ) { concedido ->
18        tienePermiso = concedido
19        if (!concedido) {
20            // Comprobar si el usuario marcó "No volver a preguntar"
21            // (solo posible desde una Activity, no directamente desde Compose)
22        }
23    }
24
25    Column(
26        modifier = Modifier.fillMaxSize().padding(16.dp),
27        horizontalAlignment = Alignment.CenterHorizontally,
28        verticalArrangement = Arrangement.Center
29    ) {
30        when {
31            tienePermiso -> {
32                Icon(
33                    Icons.Default.CameraAlt,
34                    contentDescription = null,
35                    modifier = Modifier.size(64.dp),
36                    tint = MaterialTheme.colorScheme.primary
37                )
38                Spacer(modifier = Modifier.height(16.dp))
39                Text("Permiso de cámara concedido ✓")
40                Text("Aquí iría la vista de cámara (Bloque 4)")
41            }
42
43            permisoDenegadoPermanentemente -> {
44                Icon(
45                    Icons.Default.Block,
46                    contentDescription = null,
47                    modifier = Modifier.size(64.dp),
48                    tint = MaterialTheme.colorScheme.error
49                )
50                Text("Permiso de cámara denegado permanentemente")
51                Spacer(modifier = Modifier.height(8.dp))
52                OutlinedButton(
53                    onClick = {
54                        // Abrir Ajustes del sistema para que el usuario lo conceda manualmente
55                        val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
56                            data = Uri.fromParts("package", context.packageName, null)
57                        }
58                        context.startActivity(intent)
59                    }
60                ) { Text("Abrir Ajustes") }
61            }
62
63            else -> {
64                Text(
65                    "Esta función necesita acceso a la cámara para fotografiar portadas.",
66                    style = MaterialTheme.typography.bodyMedium
67                )
68                Spacer(modifier = Modifier.height(16.dp))
69                Button(onClick = { solicitarPermiso.launch(Manifest.permission.CAMERA) }) {
70                    Text("Conceder permiso de cámara")
71                }
72            }
73        }
74    }
75}

4.4 Solicitar múltiples permisos#

 1@Composable
 2fun SolicitarPermisosMultimedia() {
 3    val context = LocalContext.current
 4
 5    // En Android 13+, los permisos de imágenes, vídeo y audio son separados
 6    val permisos = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
 7        arrayOf(
 8            Manifest.permission.READ_MEDIA_IMAGES,
 9            Manifest.permission.READ_MEDIA_VIDEO
10        )
11    } else {
12        arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE)
13    }
14
15    var permisosEstado by remember {
16        mutableStateOf(
17            permisos.associateWith { permiso ->
18                ContextCompat.checkSelfPermission(context, permiso) ==
19                    PackageManager.PERMISSION_GRANTED
20            }
21        )
22    }
23
24    // Launcher para múltiples permisos a la vez
25    val solicitarPermisos = rememberLauncherForActivityResult(
26        contract = ActivityResultContracts.RequestMultiplePermissions()
27    ) { resultados ->
28        permisosEstado = resultados
29    }
30
31    val todosConcentidos = permisosEstado.all { it.value }
32
33    Column(modifier = Modifier.padding(16.dp)) {
34        if (todosConcentidos) {
35            Text("✓ Todos los permisos concedidos")
36        } else {
37            Text("Se necesitan permisos para acceder a la galería")
38            Spacer(modifier = Modifier.height(8.dp))
39            Button(onClick = { solicitarPermisos.launch(permisos) }) {
40                Text("Solicitar permisos")
41            }
42        }
43    }
44}

5. Configuración del entorno de desarrollo#

5.1 Android Studio — aspectos clave#

Android Studio es el IDE oficial para el desarrollo Android, basado en IntelliJ IDEA. Los aspectos más importantes para el trabajo diario son:

SDK Manager (Tools → SDK Manager):

  • Gestiona las versiones del SDK de Android instaladas.
  • Para este curso: instala Android 14 (API 34) y Android 15 (API 35) como mínimo.
  • SDK Tools: asegúrate de tener instalados Android Emulator y Android SDK Platform-Tools.

Device Manager (Tools → Device Manager):

  • Crea y gestiona emuladores (AVD — Android Virtual Device).
  • Recomendado para este curso: Pixel 6 con API 34 (x86_64).

Logcat (View → Tool Windows → Logcat):

  • Muestra los logs de la app en tiempo real.
  • Filtra por tag, nivel (Verbose/Debug/Info/Warning/Error) o texto.
  • Imprescindible para depuración.

5.2 Logging con Logcat#

 1import android.util.Log
 2
 3// Niveles de log (de menor a mayor severidad)
 4Log.v("AppFlix", "Verbose: detalle exhaustivo")        // Verbose
 5Log.d("AppFlix", "Debug: información de depuración")   // Debug
 6Log.i("AppFlix", "Info: evento importante")            // Info
 7Log.w("AppFlix", "Warning: situación anómala")         // Warning
 8Log.e("AppFlix", "Error: fallo grave", excepcion)      // Error
 9
10// Ejemplo práctico
11class PeliculasRepository {
12    companion object {
13        private const val TAG = "PeliculasRepo"
14    }
15
16    suspend fun cargarPeliculas(): List<String> {
17        Log.d(TAG, "Iniciando carga de películas")
18        return try {
19            val resultado = listOf("Dune", "Inception")   // simulado
20            Log.i(TAG, "Cargadas ${resultado.size} películas correctamente")
21            resultado
22        } catch (e: Exception) {
23            Log.e(TAG, "Error al cargar películas", e)
24            emptyList()
25        }
26    }
27}

5.3 Emuladores y dispositivos reales#

Emulador:

  • Ventajas: sin necesidad de dispositivo físico, fácil de configurar, permite simular condiciones (sin red, batería baja, GPS específico).
  • Inconvenientes: más lento que un dispositivo real, algunas APIs de hardware no están disponibles (NFC, Bluetooth real).

Dispositivo real:

  • Activar Opciones de desarrollador: Ajustes → Acerca del teléfono → pulsa 7 veces en “Número de compilación”.
  • Activar Depuración USB: Opciones de desarrollador → Depuración USB.
  • Conectar por USB o mediante ADB WiFi (Android 11+: Opciones de desarrollador → Depuración inalámbrica).

5.4 Estructura de un proyecto Android#

Vista general de la estructura típica de un proyecto Android con Jetpack Compose, tipo de estructura Android:

MiApp/
├── app/
│   ├── src/
│   │   ├── main/
│   │   │   ├── java/com.ejemplo.miapp/
│   │   │   │   ├── MainActivity.kt
│   │   │   │   ├── ui/                   ← pantallas y componentes
│   │   │   │   │   ├── theme/            ← colores, tipografía, tema
│   │   │   │   │   └── components/       ← composables reutilizables
│   │   │   │   └── data/                 ← modelos y repositorios (Bloques 2-3)
│   │   │   ├── res/
│   │   │   │   ├── drawable/             ← imágenes vectoriales (SVG → XML)
│   │   │   │   ├── mipmap/               ← iconos de la app en distintas densidades
│   │   │   │   ├── values/
│   │   │   │   │   ├── strings.xml       ← textos de la app (internacionalización)
│   │   │   │   │   ├── colors.xml        ← colores base del tema
│   │   │   │   │   └── themes.xml        ← tema Material
│   │   │   │   └── xml/                  ← configuraciones XML (backup, network, etc.)
│   │   │   └── AndroidManifest.xml
│   │   ├── test/                         ← tests unitarios (JUnit)
│   │   └── androidTest/                  ← tests de instrumentación (Compose UI Test)
│   ├── build.gradle.kts                  ← dependencias y configuración del módulo app
│   └── proguard-rules.pro                ← reglas de ofuscación (release)
├── gradle/
│   └── libs.versions.toml               ← versiones centralizadas (version catalog)
├── build.gradle.kts                      ← configuración del proyecto
└── settings.gradle.kts                  ← módulos del proyecto

5.5 Version Catalog (gestión moderna de dependencias)#

Android Studio 2024+ usa un catálogo de versiones centralizado para gestionar las dependencias:

 1# gradle/libs.versions.toml
 2[versions]
 3agp = "8.7.2"
 4kotlin = "2.0.21"
 5composeBom = "2024.10.00"
 6activityCompose = "1.9.3"
 7lifecycleViewmodelCompose = "2.8.7"
 8
 9[libraries]
10androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
11androidx-ui = { group = "androidx.compose.ui", name = "ui" }
12androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
13androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
14
15[plugins]
16android-application = { id = "com.android.application", version.ref = "agp" }
17kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
18kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
1// build.gradle.kts (app) — referencia al catálogo con libs.*
2dependencies {
3    val bom = platform(libs.androidx.compose.bom)
4    implementation(bom)
5    implementation(libs.androidx.ui)
6    implementation(libs.androidx.material3)
7    implementation(libs.androidx.activity.compose)
8}

Ejemplo práctico completo: gestión de permisos en AppFlix#

Este ejemplo muestra un patrón completo y reutilizable para gestionar permisos en Compose:

  1package com.ejemplo.appflix.ui.permisos
  2
  3import android.Manifest
  4import android.content.Intent
  5import android.net.Uri
  6import android.os.Build
  7import android.provider.Settings
  8import androidx.compose.foundation.layout.*
  9import androidx.compose.material.icons.Icons
 10import androidx.compose.material.icons.filled.*
 11import androidx.compose.material3.*
 12import androidx.compose.runtime.*
 13import androidx.compose.ui.Alignment
 14import androidx.compose.ui.Modifier
 15import androidx.compose.ui.platform.LocalContext
 16import androidx.compose.ui.tooling.preview.Preview
 17import androidx.compose.ui.unit.dp
 18import androidx.core.content.ContextCompat
 19import androidx.activity.compose.rememberLauncherForActivityResult
 20import androidx.activity.result.contract.ActivityResultContracts
 21
 22// ─── PantallaGestionPermisos.kt ──────────────────────────────────────────────────────────────────
 23
 24// Estado del permiso — representado como sealed class
 25sealed class EstadoPermiso {
 26    data object Concedido : EstadoPermiso()
 27    data object Pendiente : EstadoPermiso()
 28    data object Denegado : EstadoPermiso()
 29    data object DenegadoPermanentemente : EstadoPermiso()
 30}
 31
 32@Composable
 33fun PantallaGestionPermisos() {
 34    val context = LocalContext.current
 35
 36    // Comprobación inicial del estado del permiso de cámara
 37    var estadoPermiso by remember {
 38        val concedido = ContextCompat.checkSelfPermission(
 39            context, Manifest.permission.CAMERA
 40        ) == android.content.pm.PackageManager.PERMISSION_GRANTED
 41
 42        mutableStateOf(
 43            if (concedido) EstadoPermiso.Concedido else EstadoPermiso.Pendiente
 44        )
 45    }
 46
 47    val solicitarPermiso = rememberLauncherForActivityResult(
 48        ActivityResultContracts.RequestPermission()
 49    ) { concedido ->
 50        estadoPermiso = if (concedido) EstadoPermiso.Concedido
 51                        else EstadoPermiso.Denegado
 52    }
 53
 54    Scaffold(
 55        topBar = { TopAppBar(title = { Text("Permisos de AppFlix") }) }
 56    ) { padding ->
 57        Column(
 58            modifier = Modifier
 59                .fillMaxSize()
 60                .padding(padding)
 61                .padding(24.dp),
 62            horizontalAlignment = Alignment.CenterHorizontally,
 63            verticalArrangement = Arrangement.Center
 64        ) {
 65            // Icono según estado
 66            val (icono, colorIcono, descripcion) = when (estadoPermiso) {
 67                EstadoPermiso.Concedido -> Triple(
 68                    Icons.Default.CheckCircle,
 69                    MaterialTheme.colorScheme.primary,
 70                    "Cámara disponible"
 71                )
 72                EstadoPermiso.Pendiente -> Triple(
 73                    Icons.Default.CameraAlt,
 74                    MaterialTheme.colorScheme.onSurfaceVariant,
 75                    "La app necesita acceso a la cámara para fotografiar portadas"
 76                )
 77                EstadoPermiso.Denegado,
 78                EstadoPermiso.DenegadoPermanentemente -> Triple(
 79                    Icons.Default.Block,
 80                    MaterialTheme.colorScheme.error,
 81                    "Sin acceso a la cámara"
 82                )
 83            }
 84
 85            Icon(icono, contentDescription = null, modifier = Modifier.size(72.dp), tint = colorIcono)
 86            Spacer(modifier = Modifier.height(16.dp))
 87            Text(descripcion, style = MaterialTheme.typography.bodyLarge)
 88            Spacer(modifier = Modifier.height(32.dp))
 89
 90            // Acción según estado
 91            when (estadoPermiso) {
 92                EstadoPermiso.Concedido -> {
 93                    Text("✓ Puedes usar la cámara en AppFlix")
 94                }
 95                EstadoPermiso.Pendiente -> {
 96                    Button(
 97                        onClick = { solicitarPermiso.launch(Manifest.permission.CAMERA) },
 98                        modifier = Modifier.fillMaxWidth()
 99                    ) { Text("Conceder permiso de cámara") }
100                }
101                EstadoPermiso.Denegado,
102                EstadoPermiso.DenegadoPermanentemente -> {
103                    OutlinedButton(
104                        onClick = {
105                            val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
106                                data = Uri.fromParts("package", context.packageName, null)
107                            }
108                            context.startActivity(intent)
109                        },
110                        modifier = Modifier.fillMaxWidth()
111                    ) { Text("Abrir Ajustes del sistema") }
112                }
113            }
114        }
115    }
116}
117
118@Preview(showBackground = true)
119@Composable
120fun PantallaPermsisosPreview() {
121    MaterialTheme {
122        PantallaGestionPermisos()
123    }
124}

Actividad de consolidación del Bloque 1#

Llegados a este punto, has completado los fundamentos del Bloque 1. El proyecto vertebrador AppFlix debe tener en este momento:

  • Pantalla de bienvenida con campo de nombre y botón habilitado condicionalmente (B1-T1 B).
  • Pantalla de listado con búsqueda, filtros por género y toggle de favorito (B1-T1 C).
  • Gestión básica de al menos un permiso (B1-T1 D).

Tarea de consolidación (para entregar):

Añade a la app del listado un botón “Compartir lista” que use un Intent implícito para compartir los títulos de las películas filtradas actualmente. El mensaje compartido debe incluir el número de resultados y los títulos, uno por línea.

Utiliza Intent.ACTION_SEND con type = "text/plain" y construye el texto con la lista filtrada.*


Referencias#

Calendar  Última modificación: viernes, 26 de junio de 2026