Tema 3: Intents y permisos
Objetivos de este tema
- Comprender las diferentes estrategias de navegación en aplicaciones Android.
- Dominar el uso de la clase
Intent
para la comunicación entre componentes. - Aplicar técnicas modernas para el envío de datos entre actividades y la recepción de resultados.
- Gestionar adecuadamente los permisos peligrosos en Android.
- Gestionar la navegación con
ViewModel
en el contexto de MVVM y Clean Architecture. - Integrar el patrón
ViewModel
en la gestión de la navegación y el estado de los permisos. - Comparar cuándo elegir múltiples actividades o una sola actividad con varias pantallas.
3.1. Creación y navegación entre actividades
Para crear una Activity
nueva en el modelo de vistas se utiliza una clase conocida como Intent
, esto pueden ser de dos formas:
- Explícitos, indicarán que deben lanzar exactamente, su uso típico es ejecutar diferentes componentes internos de una aplicación. Por ejemplo, una actividad (ventana nueva).
- Implícitos, se utilizan para lanzar tareas abstractas, del tipo “quiero hacer una llamada” o “quiero hacer una foto”. Estas peticiones se resuelven en tiempo de ejecución, por lo que el sistema buscará los componentes registrados para la tarea pedida, si encontrase varias, el sistema preguntará al usuario que componente prefiere.
3.1.1. Crear nuevas Activity
Para crear una Activity
es necesrio extender ComponentActivity
y usar startActivity(Intent(this, OtraActivity::class.java))
. Este sistema se utiliza para lanzar nuevas actividades en el sistema de vistas, y sería un Intent explícito_.
3.1.2. Enviar datos con Intent
Para añadir datos a la llamada se usará intent.putExtra("clave", valor)
, y se recibirá en OtraActivity
utilizando intent.getXXXExtra("clave")
. Como en la creación, se utiliza para lanzar nuevas actividades en el sistema de vistas.
3.1..3. Recibir resultados
Para recuperar datos de OtraActivity
se recomienda utilizar la API moderna, creando el siguiente callback, sustituyendo la versión anterior que utilizaba onActivityResult()
. A este método también se le conoce como Intent por contrato.
1val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
2 if (result.resultCode == Activity.RESULT_OK) { /* … */ }
3}
4launcher.launch(Intent(...))
3.2. Gestión de permisos
Cuando se quiere lanzar una tarea que no es propia de la app se tendrá que tratar con otro tema, los permisos. El tratamiento de los permisos en Android cambió a partir de la API 23, hasta entonces, se concedían durante la instalación. Ahora, debe concederse de manera explícita aquellos considerados peligrosos, ya no se pide permiso durante el proceso de instalación sino en tiempo de ejecución.
A raíz de este cambio se produce una clasificación de los permisos, básicamente se distinguirán tres tipos de permisos según sea su nivel de peligrosidad.
- Permisos normales: estos se utilizan cuando la aplicación necesita acceder a recursos o servicios fuera del ámbito de la app, donde no existe riesgo para la privacidad del usuario o para el funcionamiento de otras aplicaciones, por ejemplo, cambiar el uso horario.
Si se declara en el manifest de la aplicación uno de estos permisos, el sistema otorgará automáticamente permiso para su uso durante la instalación. Además de no preguntarse al usuario por ellos, estos no podrán revocarlos. - Permisos de firma: estos son concedidos durante la instalación de la app, pero sólo cuando la aplicación que intenta utilizar el permiso está firmada por el mismo certificado que la aplicación que define el permiso. Estos permisos no se suelen utilizar con aplicaciones de terceros, es decir, se utilizan entre aplicaciones del mismo desarrollador.
- Permisos peligrosos: estos permisos involucran áreas potencialmente peligrosas, como son la privacidad del usuario, la información almacenada por los usuarios o la interacción con otras aplicaciones. Si se declara la necesidad de uso de uno de estos permisos, se necesitará el consentimiento explícito del usuario, y se hará en tiempo de ejecución. Hasta que no se conceda el permiso, la app no podrá hacer uso de esa funcionalidad. Por ejemplo, acceder a los contactos.
Puedes encontrar todos los permisos que se pueden utilizar en la documentación de Google para Android. El valor que necesites añadir al manifest lo encontrarás en Constant Value, y Protection level indica el tipo de permiso que es.
INTERNET
public static final String INTERNET
Allows applications to open network sockets.
Protection level: normal
Constant Value: "android.permission.INTERNET"
Los permisos normales no requieren de una solicitud al usuario para poder funcionar, es por eso que se comenzarán por los permisos peligrosos, concretamente, uno de los más habituales, el uso de la cámara de fotos del dispositivo. Según la documentación de Google, cuando uno se plantea la gestión de permisos debe plantearse el siguiente flujo de trabajo para una correcta gestión.
Como estamos introduciendo el uso de Jetpack Compose, una de las cosas que cambia con respecto al sistema de vistas es la gestión de permisos, para ello se hará una primera aproximación a ViewModel
.
3.2.1. ¿Qué es un ViewModel?
Un ViewModel
es un componente de Architecture Components de Android Jetpack que permite almacenar y gestionar datos relacionados con la UI, de forma que sobreviven a cambios de configuración (como rotaciones de pantalla). Sus principales características son:
- Separar la lógica de negocio de la UI, manteniendo los Composables atómicos y centrados en la presentación.
- Mantener la consistencia del estado tras representaciones de activities o fragments.
- Diseñado para integrarse fácilmente con librerías como
Hilt
,Navigation Compose
y funciones de flujo de datos comoStateFlow
oLiveData
.
En Jetpack Compose, los ViewModel
se crearán e inyectarán en Composables utilizando las funciones viewModel()
o hiltViewModel()
.
3.2.2. Integrar permisos con ViewModel
Se creará una nueva clase (PermissionHandlerViewModel
) que extenderá (heredará) de ViewModel
y centralizará la lógica para solicitar y comprobar el estado del permiso.
1class PermissionHandlerViewModel : ViewModel() {
2 data class PermissionUiState(
3 val granted: Boolean = false,
4 val showRationale: Boolean = false,
5 val permanentlyDenied: Boolean = false
6 )
7
8 // MutableStateFlow to hold the UI state, we use backing property.
9 private val _uiState = MutableStateFlow(PermissionUiState())
10 val uiState: StateFlow<PermissionUiState> = _uiState.asStateFlow()
11
12 // Function to update the UI state based on permission results.
13 fun onPermissionResult(granted: Boolean, shouldShowRationale: Boolean) {
14 _uiState.update {
15 it.copy(
16 granted = granted,
17 showRationale = !granted && shouldShowRationale,
18 permanentlyDenied = !granted && !shouldShowRationale
19 )
20 }
21 }
22}
El método onPermissionResult
actualizará el estado según la respuesta del usuario:
- granted: permiso concedido.
- showRationale: denegado con posible explicación.
- permanentlyDenied: denegación del permiso sin posibilidad de volver a preguntar.
El uso de MutableStateFlow
en el ViewModel
es para representar un estado que pueda sobrevivir a cambios de configuración, integrarse con flujos de datos y mantenerse testable y encapsulado. Se expone como StateFlow
para su consumo externo, esta técnica se conoce como backing. En Compose, se usa collectAsState()
para convertirlo en un estado observable y disparar la recomposición.
Ahora se añadirá al Manifest
el permiso para poder utilizar la cámara de fotos del dispositivo.
1<uses-permission android:name="android.permission.CAMERA" />
2<uses-feature android:name="android.hardware.camera" android:required="true" />
Para este tipo de acciones, es necesario establecer en el Manifest
lo que se conoce como queries
, estas permiten indicar al sistema operativo que la aplicación va ha necesitar una aplicación de terceros. La siguiente querie se utiliza para indicar que la aplicación va a necesitar el uso de la cámara de fotos.
1<queries>
2 <intent>
3 <action android:name="android.media.action.IMAGE_CAPTURE" />
4 </intent>
5</queries>
A continuación, se creará el siguiente Composable para crear la pantalla principal, esto es meramente estético.
1@OptIn(ExperimentalMaterial3Api::class)
2@Preview(showBackground = true)
3@Composable
4fun MainScreen() {
5 val ctxt = LocalContext.current
6
7 Scaffold(
8 topBar = {
9 TopAppBar(
10 title = { Text(ctxt.getString(R.string.app_name)) },
11 colors = topAppBarColors(
12 containerColor = MaterialTheme.colorScheme.primaryContainer,
13 titleContentColor = MaterialTheme.colorScheme.primary
14 )
15 )
16 }
17 ) { innerPadding ->
18 Column(
19 modifier = Modifier
20 .padding(innerPadding)
21 .fillMaxWidth()
22 ) {
23 OpenCamera()
24 }
25 }
26}
Ahora se creará el método OpenCamera()
que será el encargado de gestionar los permisos y mostrar la UI según la respuesta del usuario.
1@Composable
2fun OpenCamera(viewModel: PermissionHandlerViewModel = viewModel()) {
3 val permissionState = viewModel.uiState.collectAsState() // Obtiene el estado del permiso desde el ViewModel.
4 val ctxt = LocalContext.current
5 // Este callback se usa para solicitar el permiso de cámara.
6 val requestPermission = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
7 viewModel.onPermissionResult(
8 granted, ActivityCompat.shouldShowRequestPermissionRationale(
9 ctxt as Activity, Manifest.permission.CAMERA
10 )
11 )
12 }
13
14 // Observamos el estado del permiso y actuamos en consecuencia.
15 LaunchedEffect(permissionState) {
16 when {
17 permissionState.value.granted -> {
18 // Aquí abrimos la cámara; por simplicidad indicamos con un log
19 Log.d("CameraPermission", "Acceso a cámara concedido")
20 // Podrías lanzar una navegación o mostrar vista de cámara
21 }
22
23 permissionState.value.showRationale -> {
24 // Mostrar diálogo explicativo
25 }
26
27 permissionState.value.permanentlyDenied -> {
28 // Mostrar diálogo con opción a abrir ajustes
29 }
30
31 else -> {
32 // Primer lanzamiento: solicitamos el permiso
33 requestPermission.launch(Manifest.permission.CAMERA)
34 }
35 }
36 }
37
38 // Aquí se muestra la UI dependiendo del estado del permiso.
39 when {
40 permissionState.value.granted -> {
41 Text("Pulsa el botón para abrir un intent")
42 Button(
43 onClick = {
44 Log.d("DEBUG", "Botón pulsado")
45
46 val cameraIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
47 if (cameraIntent.resolveActivity(ctxt.packageManager) != null)
48 ctxt.startActivity(cameraIntent)
49 else Log.e("DEBUG", "No hay aplicación que pueda manejar la cámara")
50 },
51 modifier = Modifier.padding(8.dp).fillMaxWidth()
52 ) {
53 Text(text = "Abrir la cámara")
54 }
55 }
56
57 permissionState.value.showRationale -> {
58 Text("Se necesita acceso a la cámara de fotos")
59 Toast.makeText(
60 ctxt,
61 "Es necesario tener acceso a la cámara de fotos",
62 Toast.LENGTH_LONG
63 ).show()
64 }
65
66 permissionState.value.permanentlyDenied -> {
67 Text("Permiso denegado permanentemente")
68 Button(
69 onClick = { // Se abren los ajustes de la aplicación para que el usuario pueda conceder el permiso manualmente.
70 ctxt.startActivity(
71 Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
72 data = Uri.fromParts("package", ctxt.packageName, null)
73 }
74 )
75 }, modifier = Modifier.padding(8.dp).fillMaxWidth()
76 ) {
77 Text("Abrir ajustes")
78 }
79 }
80
81 else -> {
82 Text("Solicitando permiso para acceder a la cámara")
83 // Aquí podrías mostrar un diálogo o una UI que explique por qué se necesita el permiso
84 }
85 }
86}
Deberás añadir la siguiente dependencia para simplificar la creación y uso de ViewModel
al build.gradle.kts (Module :app)
. Recuerda sincronizar el proyecto para que surta efecto.
1implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.9.1")
- Como habrás observado,
ViewModel
centraliza todo el estado de los permisos (concedido, con justificación, denegado permanentemente). - El Composable observa ese estado con
collectAsState()
y lanza acciones según el estado:- Solicitar permiso.
- Mostrar explicaciones.
- Abrir ajustes del sistema si hay denegación permanente.
- Ejecutar acceso a la cámara cuando esté concedido.
viewModel()
es la forma recomendada en Compose para instanciarViewModels
vinculados al ciclo de vida (debes incluir la biblioteca).- No necesitas hacer nada en el
Activity
ni pasar parámetros adicionales. - Asegura la consistencia del estado y facilitando además la integración con el patrón MVVM y clean architecture.
3.3. Intents implícitos
Pero, para abrir los Intents implícitos, se sigue utilizando la clase Intent
, a continuación, se muestran algunos ejemplos sencillos, empezando por aquellos que no requieren permiso del usuario.
3.3.1. Abrir una URL en el navegador del dispositivo
En este es necesario establecer en el Manifest
la queries
para indicar que la aplicación va a necesitar un navegador web.
1<?xml version="1.0" encoding="utf-8"?>
2<manifest xmlns:android="http://schemas.android.com/apk/res/android">
3
4 <!-- Comprueba que existe un navegador en el sistema -->
5 <queries>
6 <intent>
7 <action android:name="android.intent.action.VIEW" />
8 <category android:name="android.intent.category.BROWSABLE" />
9 <data android:scheme="https" />
10 </intent>
11 </queries>
12
13 <application...></application>
14</manifest>
El código para lanzar el Intent podría ser como el que se muestra a continuación.
1Button(
2 onClick = {
3 Log.d("DEBUG", "Botón pulsado")
4
5 // Intent para abrir un navegador web
6 Intent(Intent.ACTION_VIEW, "https://www.javiercarrasco.es".toUri()).apply {
7 if (this.resolveActivity(ctxt.packageManager) != null)
8 ctxt.startActivity(this)
9 else Log.d("DEBUG", "Hay un problema para encontrar un navegador.")
10 }
11 },
12 modifier = Modifier
13 .padding(8f.dp)
14 .fillMaxWidth()
15) {
16 Text("Abrir navegador")
17}
Puedes refactorizar el código y llevarte el código para crear el Intent
en un método a parte, al que se le pase el contexto y la URL que quieras abrir.
1fun openWebPage(ctxt: Context, url: String) {
2 // Intent para abrir un navegador web
3 Intent(Intent.ACTION_VIEW, url.toUri()).apply {
4 addCategory(Intent.CATEGORY_BROWSABLE) // Añade categoría para navegadores.
5 flags = Intent.FLAG_ACTIVITY_NEW_TASK // Asegura que se abra en una nueva tarea.
6 if (this.resolveActivity(ctxt.packageManager) != null)
7 ctxt.startActivity(this)
8 else Log.d("DEBUG", "Hay un problema para encontrar un navegador.")
9 }
10}
3.3.2. Marcar un número de teléfono
Este no necesita crear una query
, se entiende que el dispositivo está preparado, y no necesita solicitar al usuario permiso explícito.
1fun openDialer(ctxt: Context, phoneNumber: String) {
2 // Intent para abrir la aplicación de teléfono
3 Intent(Intent.ACTION_DIAL, "tel:$phoneNumber".toUri()).apply {
4 addCategory(Intent.CATEGORY_DEFAULT) // Añade categoría por defecto
5 flags = Intent.FLAG_ACTIVITY_NEW_TASK // Asegura que se abra en una nueva tarea
6 ctxt.startActivity(this)
7 }
8}
3.3.3. Abrir una aplicación de mapas
Tampoco requiere query
ni permiso específico ya que no se está utilizando geolocalización, para lo que sí sería necesario.
1fun openMap(ctxt: Context, geo: String) { // geo: "geo:0,0?q=Alicante"
2 // Intent para abrir la aplicación de teléfono
3 Intent(Intent.ACTION_VIEW, geo.toUri()).apply {
4 addCategory(Intent.CATEGORY_DEFAULT) // Añade categoría por defecto
5 flags = Intent.FLAG_ACTIVITY_NEW_TASK // Asegura que se abra en una nueva tarea
6 ctxt.startActivity(this)
7 }
8}
3.3.4. Escribir un correo electrónico
En primer lugar se creará la siguiente query
en el Manifest
.
1<!-- Comprueba que existe una aplicación de correo electrónico -->
2<queries>
3 <intent>
4 <action android:name="android.intent.action.SENDTO" />
5 <category android:name="android.intent.category.DEFAULT" />
6 <data android:scheme="mailto" />
7 </intent>
8</queries>
Un posible método para componer un correo podría ser como el siguiente.
1fun composeMail(ctxt: Context, email: String, subject: String, body: String) {
2 // Intent para enviar un correo electrónico
3 Intent(Intent.ACTION_SENDTO).apply {
4 data = "mailto:".toUri() // Asegura que solo se manejen aplicaciones de correo
5 putExtra(Intent.EXTRA_EMAIL, arrayOf(email)) // Destinatario del correo
6 // putExtra(Intent.EXTRA_CC, arrayOf(emailsCC)) // Destinatarios en copia (opcional)
7 putExtra(Intent.EXTRA_SUBJECT, subject)
8 putExtra(Intent.EXTRA_TEXT, body)
9
10 addCategory(Intent.CATEGORY_DEFAULT) // Añade categoría por defecto
11 flags = Intent.FLAG_ACTIVITY_NEW_TASK // Asegura que se abra en una nueva tarea
12
13 if (this.resolveActivity(ctxt.packageManager) != null)
14 ctxt.startActivity(Intent.createChooser(this, "Enviar correo..."))
15 else Log.d("DEBUG", "Hay un problema para enviar el correo electrónico.")
16 }
17}
3.3.5. Crear una alarma
El siguiente ejemplo necesita establecer el permiso correspondiente para poder crear una alarma en el despertador, en el manifest deberás añadir la siguiente línea. Este permiso está catalogado como normal, por tanto no se necesita pedir permiso al usuario.
1<uses-permission android:name="com.android.alarm.permission.SET_ALARM" />
También habrá que añadir la query
para buscar el tipo de aplicación necesaria.
1<!-- Comprueba que existe una aplicación para establecer alarmas -->
2<queries>
3 <intent>
4 <action android:name="android.intent.action.SET_ALARM" />
5 <category android:name="android.intent.category.DEFAULT" />
6 </intent>
7</queries>
Un posible método para establecer una alarma en la aplicación de reloj podría ser el siguiente.
1fun setAlarm(ctxt: Context, mensaje: String, hora: Int, minuto: Int) {
2 Log.d("SetAlarm", "Estableciendo alarma: $mensaje a las $hora:$minuto")
3
4 Intent(AlarmClock.ACTION_SET_ALARM).apply {
5 putExtra(AlarmClock.EXTRA_MESSAGE, mensaje)
6 putExtra(AlarmClock.EXTRA_HOUR, hora)
7 putExtra(AlarmClock.EXTRA_MINUTES, minuto)
8
9 if (this.resolveActivity(ctxt.packageManager) != null) {
10 ctxt.startActivity(this)
11 } else {
12 Log.d("DEBUG", "Hay un problema para establecer la alarma.")
13 Toast.makeText(
14 ctxt,
15 "No se pudo establecer la alarma, comprueba que tienes una aplicación de reloj instalada.",
16 Toast.LENGTH_LONG
17 ).show()
18 }
19 }
20}
A continuación, se muestra el uso de otro Intent
que sí requieren permiso del usuario, haciendo uso de la clase vista en el punto anterior. Recuerda añadir la dependencia "androidx.lifecycle:lifecycle-viewmodel-compose:2.9.1"
al Gradle.
3.3.6. Realizar una llamada telefónica
El siguiente Intent
, a diferencia del anterior, sí requiere permiso expreso por parte del usuario, ya que se va a producir una acción considerada peligrosa.
En primer lugar habrá que indicar en el Manifest
el uso del permiso en cuestión, y la necesidad del componente hardware necesario para realizar la acción.
1<uses-permission android:name="android.permission.CALL_PHONE" />
2<uses-feature android:name="android.hardware.telephony" android:required="false" />
La adaptación del método para realizar la llamada, controlando el estado de los permisos podría quedar como se muestra a continuación.
1@Composable
2fun CallPhone(phoneNumber: String, viewModel: PermissionHandlerViewModel = viewModel()) {
3 val permissionState =
4 viewModel.uiState.collectAsState() // Obtiene el estado del permiso desde el ViewModel.
5 val ctxt = LocalContext.current
6 // Este callback se usa para solicitar el permiso de cámara.
7 val requestPermission =
8 rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
9 viewModel.onPermissionResult(
10 granted, ActivityCompat.shouldShowRequestPermissionRationale(
11 ctxt as Activity, Manifest.permission.CALL_PHONE
12 )
13 )
14 }
15
16 // Se observa el estado del permiso y actuamos en consecuencia.
17 LaunchedEffect(permissionState) {
18 when {
19 permissionState.value.granted -> {
20 // Aquí abrimos la cámara; por simplicidad indicamos con un log
21 Log.d("CallPermission", "Acceso a llamar concedido")
22 }
23
24 else -> {
25 // Primer lanzamiento: solicitamos el permiso
26 requestPermission.launch(Manifest.permission.CALL_PHONE)
27 }
28 }
29 }
30
31 // Aquí se muestra la UI dependiendo del estado del permiso.
32 when {
33 permissionState.value.granted -> {
34 Text("Pulsa el botón para abrir un intent")
35 Button(
36 onClick = {
37 Log.d("DEBUG", "Botón pulsado")
38
39 // Intent para realizar una llamada telefónica
40 Intent(Intent.ACTION_CALL, "tel:$phoneNumber".toUri()).apply {
41
42 addCategory(Intent.CATEGORY_DEFAULT) // Añade categoría por defecto
43 flags = Intent.FLAG_ACTIVITY_NEW_TASK // Asegura que se abra en una nueva tarea
44
45 // Nota: ACTION_CALL requiere el permiso CALL_PHONE en el manifiesto
46 if (this.resolveActivity(ctxt.packageManager) != null)
47 ctxt.startActivity(this)
48 else Log.d("DEBUG", "Hay un problema para realizar la llamada.")
49 }
50 },
51 modifier = Modifier.padding(8.dp).fillMaxWidth()
52 ) {
53 Text(text = "Realizar llamada telefónica")
54 }
55 }
56
57 permissionState.value.showRationale -> {
58 Text("Se necesita acceso para realizar llamadas telefónicas")
59 // Solicitar nuevamente el permiso.
60 Button(
61 onClick = { // Se solicita el permiso de llamada telefónica.
62 requestPermission.launch(Manifest.permission.CALL_PHONE)
63 },
64 modifier = Modifier.padding(8.dp).fillMaxWidth(),
65 colors = ButtonDefaults.buttonColors(containerColor = Color.Red)
66 ) {
67 Text("Solicitar permiso")
68 }
69
70 Toast.makeText(
71 ctxt,
72 "Es necesario tener acceso para realizar llamadas telefónicas",
73 Toast.LENGTH_LONG
74 ).show()
75 }
76
77 permissionState.value.permanentlyDenied -> {
78 Text("Permiso denegado permanentemente")
79 Button(
80 onClick = { // Se abren los ajustes de la aplicación para que el usuario pueda conceder el permiso manualmente.
81 ctxt.startActivity(
82 Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
83 data = Uri.fromParts("package", ctxt.packageName, null)
84 }
85 )
86 }, modifier = Modifier.padding(8.dp).fillMaxWidth()
87 ) {
88 Text("Abrir ajustes")
89 }
90 }
91
92 else -> {
93 Text("Solicitando permiso para realizar llamadas telefónicas")
94 // Aquí podrías mostrar un diálogo o una UI que explique por qué se necesita el permiso
95 }
96 }
97}
Ejemplos prácticos
Ejemplo práctico 11 Recuperar la imagen capturada desde la cámara de fotos
3.4. Creación y navegación entre actividades
Jetpack Compose apuesta por el uso de una sola Activity
con múltiples pantallas (Composable), pero, en algunas situaciones es útil o necesario utilizar varias actividades: interoperabilidad con vistas heredadas, flujos aislados o necesidades de integración específicas.
3.4.1. Crear nuevas Activity
En Android, una actividad representa una pantalla completa. Crear una nueva Activity
en un proyecto con Jetpack Compose es algo más sencillo que hacerlo en el sistema basado en vistas, ya que no tienes que crear el XML que la represente. El primer paso será crear una nueva clase. Puedes utilizar la opción File > New > Compose > Empty Activity. Tras eliminar algo de boilerplate code, podría quedar así.
1class SecondActivity : ComponentActivity() {
2 override fun onCreate(savedInstanceState: Bundle?) {
3 super.onCreate(savedInstanceState)
4 enableEdgeToEdge()
5 setContent {
6 Text(
7 "Second Activity",
8 modifier = Modifier.padding(32.dp)
9 )
10 }
11 }
12}
Esta nueva actividad quedará registrada en el Manifest.
1<activity
2 android:name=".SecondActivity"
3 android:exported="false"
4 android:label="@string/title_activity_second"
5 android:theme="@style/Theme.DocumentationT3_4" />
3.4.2. Enviar datos con Intent
Una vez preparada una actividad principal sencilla con un botón que traslade al usuario a la segunda actividad (SecondActivity.kt
)…
1class MainActivity : ComponentActivity() {
2 override fun onCreate(savedInstanceState: Bundle?) {
3 super.onCreate(savedInstanceState)
4 enableEdgeToEdge()
5 setContent {
6 DocumentationT3_4Theme {
7 MainScreen()
8 }
9 }
10 }
11}
12
13@OptIn(ExperimentalMaterial3Api::class)
14@Preview(showBackground = true)
15@Composable
16fun MainScreen() {
17 val ctxt = LocalContext.current
18 Scaffold(
19 topBar = {
20 TopAppBar(
21 title = { Text(ctxt.getString(R.string.app_name)) },
22 colors = topAppBarColors(
23 containerColor = MaterialTheme.colorScheme.primaryContainer,
24 titleContentColor = MaterialTheme.colorScheme.primary,
25 )
26 )
27 }
28 ) { innerPadding ->
29 Column(modifier = Modifier.padding(innerPadding)) {
30 Text(
31 modifier = Modifier.padding(5.dp).fillMaxWidth(),
32 text = "Main Activity"
33 )
34 Button(
35 modifier = Modifier.padding(5.dp).fillMaxWidth(),
36 onClick = {
37 /* Navigate to SecondActivity */
38 }) {
39 Text(text = "Ir a la segunda pantalla")
40 }
41 }
42 }
43}
…pueden pasarse datos de una Activity
a otra usando el sistema de Intent
. El onClick
del botón podría ser como se muestra a continuación.
1/* Navigate to SecondActivity */
2Intent(ctxt, SecondActivity::class.java).apply {
3 putExtra("nombre", "Javier")
4 putExtra("edad", 48)
5
6 ctxt.startActivity(this)
7}
Ahora, en la segunda actividad (SecondActivity.kt
) se pueden recoger los datos y mostrarlos en un Text
por ejemplo.
1class SecondActivity : ComponentActivity() {
2 override fun onCreate(savedInstanceState: Bundle?) {
3 super.onCreate(savedInstanceState)
4 enableEdgeToEdge()
5 setContent {
6 // Se recuperan los datos enviados desde MainActivity.
7 val nombre = intent.getStringExtra("nombre") ?: ""
8 val edad = intent.getIntExtra("edad", 0)
9
10 Column(modifier = Modifier.padding(32.dp)) {
11 Text("Second Activity")
12 Spacer(modifier = Modifier.height(8.dp))
13 if (nombre.isNotEmpty())
14 Text("Nombre: $nombre")
15 if (edad > 0)
16 Text("Edad: $edad")
17 }
18 }
19 }
20}
Consejo
Utiliza constantes o companion object
para definir las claves (“nombre”, “edad”), esto te evitará posibles errores de escritura.
3.4.3. Recibir resultados con ActivityResultLauncher
Desde la API 30 de Android (Jetpack Activity 1.2.0), se utiliza la nueva API para recibir resultados, evitando el uso obsoleto de onActivityResult
.
El primer paso será crear en un Composable el launcher encargado de lanzar la nueva actividad y recoger el resultado devuelto.
1val launcher = rememberLauncherForActivityResult(
2 contract = ActivityResultContracts.StartActivityForResult()
3) { result ->
4 if (result.resultCode == Activity.RESULT_OK) {
5 println("Resultado recibido de SecondActivity")
6 val data = result.data?.getStringExtra("resultado")
7
8 if (data != null) {
9 // Aquí puedes manejar el resultado que viene de SecondActivity
10 // Por ejemplo, mostrar un Toast o actualizar la UI
11 println("Resultado recibido: $data")
12 }
13 }
14}
El segundo paso será lanzar el Intent
, el método completo quedaría como se muestra a continuación.
1@Composable
2fun NavigateToSecondActivity(nombre: String, edad: Int) {
3 val ctxt = LocalContext.current
4 val launcher = rememberLauncherForActivityResult(
5 contract = ActivityResultContracts.StartActivityForResult()
6 ) { result ->
7 if (result.resultCode == Activity.RESULT_OK) {
8 println("Resultado recibido de SecondActivity")
9 val data = result.data?.getStringExtra("resultado")
10
11 if (data != null) {
12 // Aquí puedes manejar el resultado que viene de SecondActivity
13 // Por ejemplo, mostrar un Toast o actualizar la UI
14 println("Resultado recibido: $data")
15 }
16 }
17 }
18
19 Button(
20 modifier = Modifier.padding(5.dp).fillMaxWidth(),
21 onClick = {
22 /* Navigate to SecondActivity */
23 val intent = Intent(ctxt, SecondActivity::class.java)
24 intent.putExtra("nombre", nombre)
25 intent.putExtra("edad", edad)
26
27 launcher.launch(intent)
28 }) {
29 Text(text = "Ir a la segunda pantalla")
30 }
31}
En SecondActivity.kt
se añadirá un botón, por ejemplo, que se encargue de realizar el retorno con el paso de información.
1Button(
2 onClick = {
3 // Se crea un Intent para devolver el resultado a MainActivity.
4 val resultIntent = Intent().apply {
5 putExtra("resultado", "Hola $nombre, tienes $edad años")
6 }
7 setResult(Activity.RESULT_OK, resultIntent)
8 finish() // Finaliza SecondActivity y devuelve el resultado.
9 }
10) {
11 Text("Devolver resultado")
12}
El uso de esta API es compatible con Compose y es segura para el ciclo de vida.
3.4.4. ¿Cuándo usar varias Activity?
En Jetpack Compose se recomiendoa el uso múltiples actividades solo si:
- Se esta integrando módulos legacy con Compose.
- Se necesita un fuerte aislamiento entre pantallas (como flujos separados de autenticación o ajustes).
- Se necesitas interoperar con componentes que requieran una Activity concreta (por ejemplo, bibliotecas de terceros).
En todos los demás casos, se recomienda usar una sola actividad y Navigation Compose
para gestionar pantallas.