La API Photo Picker de Android en 2026: adjuntar una foto sin dar acceso a toda la galería
Una guía práctica de la API Photo Picker de Android: selección única y múltiple, filtrado por MIME, el respaldo para versiones previas a Android 13 y por qué supera a READ_MEDIA_IMAGES.
Pedirle a un usuario permiso para ver toda su biblioteca de fotos solo para que pueda adjuntar un recibo a un gasto es pedir mucho más de lo que la función realmente necesita. READ_MEDIA_IMAGES le da a tu app acceso permanente a cada foto del dispositivo —fotos de vacaciones, capturas de mensajes de otras personas, lo que sea que haya ahí— para una función que solo necesitaba la única imagen que el usuario eligió. Ese desajuste es exactamente lo que el Photo Picker de Android fue creado para resolver.
Lo añadí a Granyn para que un usuario pudiera adjuntar la foto de un recibo a un gasto registrado. Así es como funciona realmente la API, y por qué hizo que la solicitud de permiso desapareciera por completo.
El método anterior, y por qué es un problema
Antes del Photo Picker, “dejar que el usuario elija una foto” significaba uno de dos caminos: solicitar READ_MEDIA_IMAGES (o READ_EXTERNAL_STORAGE antes de Android 13) y consultar MediaStore tú mismo, o lanzar ACTION_GET_CONTENT y esperar que el selector de documentos del sistema se comportara de forma consistente entre las distintas capas de personalización de cada fabricante. El camino del permiso funciona, pero tiene un costo:
- Una solicitud de permiso en tiempo de ejecución para una función fácil de rechazar por costumbre, lo que mata silenciosamente la función para una parte de los usuarios.
- El flujo de declaración de Permisos Sensibles de Play Console: un acceso amplio a medios atrae más escrutinio durante la revisión.
- Un acceso permanente que tienes que justificar en tu política de privacidad, aunque tu app solo llegue a tocar la única foto que el usuario seleccionó.
Nada de eso le da algo al usuario a cambio. La función solo necesita acceso de lectura a una sola imagen, una sola vez.
Cómo funciona realmente el Photo Picker
El Photo Picker es una interfaz propiedad del sistema: se ejecuta en un proceso separado y de confianza, y la imagen que el usuario toca se entrega a tu app como un URI content://. Tu app nunca recibe un permiso de acceso a la galería; recibe exactamente ese único URI, limitado a esa foto, durante el tiempo que lo necesites.
La configuración no requiere ningún permiso en el manifiesto para una selección única:
class AddExpenseFragment : Fragment() {
private val pickImage = registerForActivityResult(
ActivityResultContracts.PickVisualMedia(),
) { uri: Uri? ->
if (uri != null) attachReceiptPhoto(uri)
}
private fun launchPicker() {
pickImage.launch(
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly),
)
}
}
ActivityResultContracts.PickVisualMedia.ImageOnly filtra el selector para mostrar solo imágenes; VideoOnly y SingleMimeType("image/png") cubren los otros casos comunes. Sin entrada <uses-permission>, sin solicitud en tiempo de ejecución, sin diálogo de justificación que escribir.
Seleccionar más de una foto
Un recibo suele ser una sola foto, pero el mismo contrato tiene una variante de selección múltiple, con un límite que tú eliges:
private val pickImages = registerForActivityResult(
ActivityResultContracts.PickMultipleVisualMedia(maxItems = 5),
) { uris: List<Uri> ->
uris.forEach { attachReceiptPhoto(it) }
}
La interfaz del sistema impone el límite por sí misma: deshabilita más toques una vez que el usuario alcanza el máximo, en lugar de que tu código tenga que rechazar una lista más larga después del hecho.
El URI que recibes no es permanente
Este es el detalle con el que la gente tropieza: el URI content:// que se entrega a tu ActivityResultCallback solo tiene garantizada la lectura durante la duración de esa llamada. Si guardas la cadena del URI en tu base de datos e intentas abrirla días después, puede lanzar una SecurityException: el permiso no sobrevive más allá de la sesión del selector, a diferencia de un URI persistido del Storage Access Framework.
La solución es copiar los bytes que necesitas de inmediato, a un archivo propio de tu app:
private fun attachReceiptPhoto(sourceUri: Uri) {
val destFile = File(requireContext().filesDir, "receipts/${UUID.randomUUID()}.jpg")
destFile.parentFile?.mkdirs()
requireContext().contentResolver.openInputStream(sourceUri)?.use { input ->
destFile.outputStream().use { output -> input.copyTo(output) }
}
viewModel.setReceiptPath(destFile.absolutePath)
}
Haz la copia fuera del hilo principal —es E/S de archivos—, pero hazla antes de que retorne el callback de resultado de la Activity del selector, no en una pantalla posterior. Una vez copiados los bytes, el permiso transitorio content:// ya no importa; tu app ahora es dueña de un archivo privado que puede leer durante el tiempo que necesite.
El respaldo para versiones anteriores a Android 13
El Photo Picker llegó como función del sistema en Android 13, pero tiene un respaldo hasta API 21+ a través de Google Play services: la misma llamada a ActivityResultContracts.PickVisualMedia se resuelve automáticamente hacia ese respaldo en dispositivos más antiguos que tengan Play services instalado. Hay algo que vale la pena comprobar explícitamente si tu conjunto mínimo de funciones realmente requiere el comportamiento del selector nativo (como la integración de proveedores de medios en la nube por app):
val isPhotoPickerAvailable = ActivityResultContracts.PickVisualMedia
.isPhotoPickerAvailable(requireContext())
En la práctica no necesitas hacer esa comprobación para un flujo simple de “adjuntar una foto”: el contrato se degrada con elegancia por sí solo. Importa cuando decides mostrar pistas de interfaz específicas del selector que solo tienen sentido en la versión nativa.
Lo que realmente importó
La ganancia aquí no es un nuevo gesto ni un selector más bonito: es que el modelo de permisos por fin coincide con lo que hace la función. Un usuario que adjunta la foto de un recibo nunca debería haber dado acceso permanente a todo su carrete, y hasta que existió el Photo Picker, Android no tenía una forma limpia de evitarlo. Cambiar READ_MEDIA_IMAGES por PickVisualMedia eliminó un permiso del manifiesto, una solicitud en tiempo de ejecución y un párrafo de mi política de privacidad, y la función funciona exactamente igual desde el punto de vista del usuario, solo que sin pedir más de lo necesario.
// Lecturas relacionadas
Más del diario
Autenticación biométrica en Android en 2026: BiometricPrompt, el Keystore y cómo bloquear una app local-first
Una guía práctica de androidx.biometric en Android — BiometricPrompt, claves respaldadas por CryptoObject, respaldo con credenciales del dispositivo, y los errores que hacen que una huella dactilar no proteja nada.
Health Connect en Android en 2026: leer y escribir datos sin renunciar al local-first
Una guía práctica sobre la API Health Connect de Android en 2026 — permisos, lecturas en segundo plano y por qué las apps local-first deberían tratarla como opcional, no como fuente de verdad.
androidx.startup en 2026: ordenar la inicialización de librerías sin una pila de ContentProviders
Una guía práctica de la librería App Startup de Android — consolidar initializers en un único ContentProvider, declarar dependencias, y los casos de init perezosa que no puede sustituir.