El sistema de intents: explícitos, implícitos y la resolución
El intent es la unidad de acoplamiento tardío sobre la que se sostiene todo Android: un objeto que describe una intención en lugar de un destinatario, y un sistema operativo que actúa de intermediario entre quien pide y quien puede responder. Esta lección separa el intent explícito, que nombra un componente concreto y no admite ambigüedad, del implícito, que describe una acción y delega en la plataforma la elección del ejecutor. Reconstruye el algoritmo de resolución que aplica el gestor de paquetes sobre la acción, los datos y las categorías, explica por qué la visibilidad de paquetes que introdujo Android 11 convirtió comprobaciones antes inocuas en fallos silenciosos, y detalla el selector de aplicaciones, la preferencia del usuario y las razones de diseño que hacen que esa preferencia nunca pertenezca a quien envía el intent.
Casi todo lo que una aplicación de Android hace fuera de sus propios píxeles pasa por un objeto sorprendentemente humilde. Abrir una foto, compartir un texto, elegir un contacto, componer un correo, ver una dirección en un mapa, arrancar la segunda pantalla de tu propia aplicación: todo eso es la misma operación repetida con parámetros distintos, y esa operación consiste en construir una descripción de lo que se quiere que ocurra y entregársela al sistema. El nombre de esa descripción es Intent, y su rasgo definitorio no es lo que contiene sino lo que deliberadamente omite. Un intent puede decir quién debe atenderlo, pero no está obligado a hacerlo, y esa opcionalidad es la que convierte a Android en un sistema de aplicaciones componibles en lugar de un conjunto de silos que solo se comunican por la red. Entender bien los intents no es aprender una API: es entender el mecanismo por el que unas aplicaciones ofrecen capacidades a otras sin conocerse.
- Distinguir el intent explícito del implícito y saber cuál es la herramienta correcta en cada frontera.
- Reconstruir el algoritmo de resolución que el gestor de paquetes aplica sobre acción, datos y categorías.
- Declarar la visibilidad de paquetes con
queriesy evitar los fallos silenciosos posteriores a Android 11. - Usar el selector de aplicaciones con criterio y entender por qué la preferencia del usuario no te pertenece.
Dos formas de nombrar un destino
Un intent explícito lleva dentro un ComponentName, es decir, el nombre del paquete y el nombre de la clase del componente que debe atenderlo. No hay resolución, no hay candidatos y no hay ambigüedad: el sistema comprueba permisos y exportación, y entrega. Es la forma correcta de moverse dentro de tu propia aplicación y la única forma segura de dirigirse a un componente concreto de otra, porque cierra por completo la posibilidad de que un tercero intercepte el mensaje.
Un intent implícito no nombra a nadie. Describe una acción, opcionalmente unos datos y opcionalmente unas categorías, y deja que la plataforma busque quién ha declarado ser capaz de atender esa combinación. Es la forma correcta de pedir una capacidad que no te pertenece: ver una página, enviar un texto, seleccionar una imagen. La diferencia entre ambos no es de comodidad sino de contrato. En el explícito el contrato es una clase; en el implícito es un vocabulario público.
// explicito: nombro exactamente al componente que debe responder
val explicito = Intent(context, DetalleActivity::class.java).apply {
putExtra(EXTRA_ARTICULO, 42L)
}
startActivity(explicito)
// implicito: describo que quiero, el sistema decide quien puede hacerlo
val implicito = Intent(Intent.ACTION_VIEW).apply {
data = "https://ejemplo.com/articulo/42".toUri()
}
startActivity(implicito)
El objeto transporta además tres cosas que conviene no confundir. Los extras son un Bundle de carga útil, invisibles para la resolución: nadie decide nada mirándolos. El tipo MIME y el dato, en cambio, sí participan en la resolución y son parte del contrato público. Y las banderas, que no describen la intención sino la mecánica del arranque —cómo interactúa el destino con la pila de tareas y qué permisos temporales viajan con el mensaje— pertenecen a un plano distinto y se estudian con la navegación entre tareas.
Existe además una posición intermedia que resuelve limpiamente muchos casos y que se usa muy poco: fijar el paquete de destino con setPackage sin nombrar la clase. El intent sigue siendo implícito en cuanto a qué componente lo atenderá, de modo que la otra aplicación conserva la libertad de reorganizar sus actividades internamente, pero deja de ser público, porque solo se consultan los filtros de ese paquete. Es la forma correcta de hablar con una aplicación concreta cuyo vocabulario conoces sin acoplarte a su estructura interna ni exponer el mensaje al resto del dispositivo.
// ni explicito ni publico: implicito acotado a un paquete conocido
val acotado = Intent(Intent.ACTION_VIEW).apply {
data = documento
setPackage("com.ejemplo.visor")
}
El algoritmo de resolución
Cuando llega un intent sin componente, el gestor de paquetes recorre los filtros declarados por las aplicaciones instaladas y aplica tres pruebas independientes. Las tres deben pasarse; basta con que una falle para que el candidato quede descartado.
La prueba de acción exige que el filtro declare exactamente la acción del intent. Si el intent no lleva acción, cualquier filtro que declare al menos una la supera. La prueba de datos compara el esquema, el anfitrión, el puerto, la ruta y el tipo MIME, y es la más sutil de las tres porque un filtro que solo declara un esquema acepta cualquier ruta bajo él, mientras que un intent con tipo MIME nunca casará con un filtro que no declare tipos. La prueba de categorías es la más severa y la que más sorprende: todas las categorías del intent deben estar declaradas en el filtro, aunque el filtro declare además otras que el intent no lleva.
flowchart TD
A[Intent construido] --> B{Lleva ComponentName}
B -->|si| C[Entrega directa sin resolucion]
B -->|no| D[Prueba de accion]
D --> E[Prueba de datos esquema anfitrion ruta y tipo]
E --> F[Prueba de categorias]
F --> G{Cuantos candidatos quedan}
G -->|cero| H[ActivityNotFoundException]
G -->|uno| I[Arranque directo]
G -->|varios| J[Preferencia guardada o dialogo de eleccion]Hay una asimetría deliberada en ese algoritmo que merece atención. Para las actividades arrancadas con startActivity, el sistema añade en silencio la categoría CATEGORY_DEFAULT al intent implícito, lo que significa que un filtro que no la declare jamás podrá responder a un intent implícito por mucho que su acción coincida. Es la vía por la que la plataforma distingue entre un filtro que existe para que el lanzador dibuje un icono y un filtro que existe para ofrecer una capacidad a otras aplicaciones. Muchas horas de depuración perdidas terminan en esta única línea ausente del manifiesto.
Conviene añadir que el resultado de la resolución no es un ganador sino una lista ordenada, y que el orden importa menos de lo que la intuición sugiere. El atributo de prioridad que puede declararse en un filtro solo ordena candidatos dentro de la misma aplicación y no sirve para adelantar a nadie más, porque de lo contrario bastaría con declarar una prioridad enorme para secuestrar cualquier acción del sistema. Ese detalle, aparentemente menor, es la primera aparición de una constante que atraviesa el nivel entero: la plataforma nunca deja que el emisor decida quién gana un empate.
Durante años el patrón recomendado fue preguntar con resolveActivity si alguien podía atender el intent antes de lanzarlo. Desde el nivel de API 30 la visibilidad de paquetes es restringida: tu aplicación solo ve las que el sistema considera relevantes o las que declaras explícitamente. La consecuencia es cruel porque no es un error de compilación ni una excepción: la comprobación devuelve nulo, tu código concluye que no hay ninguna aplicación capaz, y la funcionalidad desaparece en silencio en dispositivos modernos mientras sigue funcionando en el emulador antiguo del equipo. La solución es declarar queries en el manifiesto o, mejor todavía, abandonar la comprobación previa y capturar ActivityNotFoundException, que es lo único que nunca miente.
Cuando de verdad necesitas inspeccionar el resultado de la resolución en lugar de limitarte a lanzar, la consulta explícita al gestor de paquetes devuelve la lista completa de candidatos con su información de presentación, y es lo que permite construir una interfaz propia de elección. Está sujeta a las mismas restricciones de visibilidad, sirve para diagnosticar por qué un intent no resuelve, y conviene tratarla como herramienta de depuración antes que como pieza de producción: una interfaz de elección propia compite con la del sistema y casi siempre pierde en confianza y en funcionalidad.
<manifest>
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
<package android:name="com.otra.app.concreta" />
</queries>
</manifest>
El selector y la preferencia que no es tuya
Cuando la resolución deja varios candidatos, el sistema no elige por ti: muestra un diálogo de desambiguación con las opciones y deja que el usuario decida, ofreciéndole además fijar esa decisión como preferencia permanente. A partir de ese momento tu intent implícito irá directo a la aplicación elegida sin volver a preguntar, y ni tú ni el resto de candidatos tenéis manera de revertirlo.
Ese último punto sorprende a mucha gente y conviene entenderlo bien: no existe ninguna API para consultar, fijar ni borrar la preferencia que el usuario asignó a una acción. Solo él puede cambiarla, y solo desde los ajustes del sistema. Cualquier diseño que dependa de recuperar esa posición está condenado desde el principio.
Existe una segunda vía, createChooser, que fuerza el diálogo aunque haya preferencia guardada. La distinción entre ambas no es cosmética. Lanzar el intent desnudo respeta el hábito del usuario y es lo correcto para acciones repetitivas como abrir un enlace. Envolverlo en el selector es lo correcto cuando la elección forma parte del gesto —compartir es siempre compartir con alguien— y por eso el sistema construye ahí una hoja de compartición con destinatarios directos y sugerencias, que es una experiencia mucho más rica que una simple lista de aplicaciones.
val envio = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, enlaceDelArticulo)
}
try {
// el selector se muestra siempre, aunque exista preferencia guardada
startActivity(Intent.createChooser(envio, null))
} catch (e: ActivityNotFoundException) {
mostrarMensajeDeCapacidadAusente()
}
La hoja de compartición moderna, además, ya no lista solo aplicaciones: ofrece destinatarios concretos —una conversación, un contacto, una carpeta— que las aplicaciones publican como atajos con una categoría reservada. Participar en ese mecanismo desde el lado receptor es lo que hace que tu aplicación aparezca en la fila superior en lugar de en la lista genérica, y el salto en uso que produce esa diferencia de posición es de otro orden de magnitud. Es, hasta donde llega el sistema de intents, la única forma legítima de mejorar tu sitio en el diálogo: aportar información útil, nunca reclamar prioridad.
El selector admite además refinamientos que casi nadie explota y que mejoran mucho el resultado: se le pueden añadir opciones iniciales propias que aparecen destacadas antes que el resto, y se le pueden excluir componentes concretos, típicamente los de tu propia aplicación, para no ofrecerle al usuario compartir consigo mismo. Ambas cosas se declaran como extras del intent selector y no requieren ninguna infraestructura adicional.
Es tentador leer el diálogo de elección como una barrera que protege el contenido del intent, y no lo es en absoluto. Cuando se muestra la lista, el sistema ya ha resuelto la coincidencia contra todos los filtros instalados, y aunque el usuario elija una sola aplicación, cualquier otra que declare el filtro adecuado ha demostrado su capacidad de aparecer ahí. Lo que el selector protege es la voluntad del usuario sobre qué aplicación ejecuta la acción, no la confidencialidad de lo que viaja dentro. Todo dato que no puedas permitirte que llegue a un destinatario cualquiera debe viajar por un intent explícito, y esa regla no admite excepciones cómodas.
Explícito
Dentro de tu aplicación y hacia componentes concretos. Sin resolución, sin interceptación posible, sin sorpresas.
Implícito
Hacia capacidades que no controlas. El vocabulario público es el contrato y la plataforma es el intermediario.
Visibilidad
Sin queries, tu aplicación es ciega a buena parte del sistema desde el nivel de API 30.
Selector
createChooser cuando elegir es parte del gesto; intent desnudo cuando el hábito del usuario debe mandar.
Conviene mirar el sistema de intents con la lente correcta, porque la incorrecta —tratarlo como una forma barroca de invocar una función remota— produce código que funciona pero no entiende dónde está. Un intent implícito no es una llamada: es una proposición sobre el mundo. Dice que existe un usuario que quiere ver un documento, o compartir un texto, o elegir una imagen, y no dice absolutamente nada sobre quién debería satisfacer ese deseo, porque quien lo emite no tiene ni la información ni la legitimidad para decidirlo. Lo que Android hace con esa proposición es lo que hace un sistema operativo con cualquier recurso compartido: arbitrar. Y el arbitraje es aquí de una naturaleza poco habitual en informática, porque el criterio último no es técnico sino la voluntad de una persona expresada en un diálogo. Esa arquitectura tiene una consecuencia que atraviesa toda la plataforma y que conviene enunciar sin rodeos: el acoplamiento tardío no es una técnica de flexibilidad, es una distribución de poder. Cuando tu aplicación emite un intent explícito está ejerciendo autoridad sobre el destino, y por eso solo puede hacerlo dentro de su propio territorio o con permiso expreso. Cuando emite uno implícito está renunciando a esa autoridad y transfiriéndosela al usuario, que es exactamente el motivo por el que no puedes recuperar la preferencia que él fijó, ni saber qué aplicaciones hay instaladas sin declararlo, ni evitar que mañana otra aplicación se declare capaz de atender lo que hoy atiendes tú en exclusiva. Cada restricción que la plataforma ha ido añadiendo en la última década —la categoría por defecto obligatoria, la visibilidad de paquetes, la verificación de dominios, los permisos temporales sobre datos— apunta en la misma dirección: hacer que el vocabulario siga siendo público y el arbitraje siga siendo del usuario, incluso a costa de romper el código de todo el mundo. Diseñar bien con intents consiste, entonces, en aceptar esa asimetría en lugar de pelearse con ella. Nombra explícitamente lo que te pertenece, describe implícitamente lo que no, y no construyas nunca una funcionalidad cuya corrección dependa de que un tercero concreto esté instalado, porque en un sistema de acoplamiento tardío esa suposición no es un atajo: es un fallo esperando a que alguien desinstale una aplicación.
- Construye el mismo intent en versión explícita e implícita hacia una pantalla propia y compara qué ocurre al renombrar la clase y al retirar la categoría por defecto del filtro.
- Emite un intent con dos categorías cuando el filtro solo declara una, comprueba que no resuelve, y explica por escrito por qué la prueba es asimétrica.
- Ejecuta un
resolveActivityen un dispositivo con nivel de API 30 o superior sin declararqueriesy observa el fallo silencioso. Arréglalo por las dos vías y decide cuál prefieres mantener. - Lanza una acción de compartir con y sin
createChooserdespués de fijar una preferencia, y documenta la diferencia en la experiencia resultante. - Recorre tu propia aplicación y clasifica cada salida al exterior según si depende de una capacidad genérica o de una aplicación concreta. Convierte en degradación elegante todos los casos del segundo grupo.