Introduction
Picking images from the Android gallery has evolved significantly. The modern approach uses the Activity Result API with ActivityResultContracts.PickVisualMedia (Android 13+ Photo Picker) or GetContent for broader compatibility. The legacy startActivityForResult() with ACTION_PICK is deprecated since API 30. The Photo Picker provides a privacy-friendly experience without requiring storage permissions, while GetContent works on older Android versions. This article covers both modern and legacy approaches.
Modern Approach: Photo Picker (Android 13+)
1// Activity or Fragment
2class MainActivity : AppCompatActivity() {
3
4 // Single image picker
5 private val pickImage = registerForActivityResult(
6 ActivityResultContracts.PickVisualMedia()
7 ) { uri ->
8 if (uri != null) {
9 imageView.setImageURI(uri)
10 Log.d("PhotoPicker", "Selected URI: $uri")
11 } else {
12 Log.d("PhotoPicker", "No image selected")
13 }
14 }
15
16 // Multiple image picker
17 private val pickMultipleImages = registerForActivityResult(
18 ActivityResultContracts.PickMultipleVisualMedia(5) // Max 5 images
19 ) { uris ->
20 if (uris.isNotEmpty()) {
21 uris.forEach { uri ->
22 Log.d("PhotoPicker", "Selected: $uri")
23 }
24 }
25 }
26
27 fun selectImage() {
28 pickImage.launch(
29 PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
30 )
31 }
32
33 fun selectMultipleImages() {
34 pickMultipleImages.launch(
35 PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
36 )
37 }
38}
The Photo Picker does not require any storage permissions. It runs in a separate process and only grants access to the specific files the user selects.
Using GetContent (Broader Compatibility)
1class MainActivity : AppCompatActivity() {
2
3 private val pickImage = registerForActivityResult(
4 ActivityResultContracts.GetContent()
5 ) { uri: Uri? ->
6 uri?.let {
7 imageView.setImageURI(it)
8 processImage(it)
9 }
10 }
11
12 // Multiple images
13 private val pickMultipleImages = registerForActivityResult(
14 ActivityResultContracts.GetMultipleContents()
15 ) { uris: List<Uri> ->
16 uris.forEach { uri ->
17 Log.d("Gallery", "Selected: $uri")
18 }
19 }
20
21 fun selectImage() {
22 pickImage.launch("image/*") // MIME type filter
23 }
24
25 fun selectPhotosOnly() {
26 pickImage.launch("image/jpeg") // Only JPEG
27 }
28
29 fun selectMultiple() {
30 pickMultipleImages.launch("image/*")
31 }
32}
GetContent uses ACTION_GET_CONTENT internally and works on Android 4.4+. It opens the system file picker or gallery app.
Processing the Selected Image
1private fun processImage(uri: Uri) {
2 // Get file size
3 contentResolver.query(uri, null, null, null, null)?.use { cursor ->
4 val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
5 val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
6 cursor.moveToFirst()
7 val fileName = cursor.getString(nameIndex)
8 val fileSize = cursor.getLong(sizeIndex)
9 Log.d("Image", "Name: $fileName, Size: $fileSize bytes")
10 }
11
12 // Read as bitmap
13 val bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
14 val source = ImageDecoder.createSource(contentResolver, uri)
15 ImageDecoder.decodeBitmap(source)
16 } else {
17 MediaStore.Images.Media.getBitmap(contentResolver, uri)
18 }
19
20 // Copy to app-specific storage
21 val outputFile = File(filesDir, "selected_image.jpg")
22 contentResolver.openInputStream(uri)?.use { input ->
23 outputFile.outputStream().use { output ->
24 input.copyTo(output)
25 }
26 }
27}
Jetpack Compose
1@Composable
2fun ImagePickerScreen() {
3 var imageUri by remember { mutableStateOf<Uri?>(null) }
4
5 val launcher = rememberLauncherForActivityResult(
6 contract = ActivityResultContracts.PickVisualMedia()
7 ) { uri ->
8 imageUri = uri
9 }
10
11 Column(
12 modifier = Modifier.fillMaxSize(),
13 horizontalAlignment = Alignment.CenterHorizontally
14 ) {
15 Button(onClick = {
16 launcher.launch(
17 PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
18 )
19 }) {
20 Text("Select Image")
21 }
22
23 imageUri?.let { uri ->
24 AsyncImage(
25 model = uri,
26 contentDescription = "Selected image",
27 modifier = Modifier.size(300.dp)
28 )
29 }
30 }
31}
In Jetpack Compose, use rememberLauncherForActivityResult() to register the picker. The AsyncImage composable (from Coil) handles loading and displaying the image.
Legacy Approach (Deprecated)
1// DEPRECATED — shown for reference only
2class LegacyActivity : AppCompatActivity() {
3
4 private val REQUEST_CODE_PICK_IMAGE = 1001
5
6 fun selectImage() {
7 val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
8 startActivityForResult(intent, REQUEST_CODE_PICK_IMAGE)
9 }
10
11 override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
12 super.onActivityResult(requestCode, resultCode, data)
13 if (requestCode == REQUEST_CODE_PICK_IMAGE && resultCode == RESULT_OK) {
14 val uri = data?.data
15 uri?.let { imageView.setImageURI(it) }
16 }
17 }
18}
startActivityForResult() and onActivityResult() are deprecated. Use the Activity Result API (registerForActivityResult) instead.
Common Pitfalls
URI permissions expire: URIs from the picker are temporary. If you need persistent access, copy the file to app storage or use takePersistableUriPermission() with ACTION_OPEN_DOCUMENT. Do not store the URI for later use without persisting permissions.
Not handling null URIs: The user can press back without selecting an image. Always check for null in the result callback before processing.
Using deprecated startActivityForResult: This API is deprecated and will eventually be removed. Migrate to registerForActivityResult() with ActivityResultContracts.
Bitmap memory issues: Loading full-resolution images directly into memory causes OutOfMemoryError. Use BitmapFactory.Options with inSampleSize to downsample, or use libraries like Glide or Coil for efficient image loading.
Missing Photo Picker backport: PickVisualMedia requires Google Play Services on devices running Android 11-12. Devices without Play Services fall back to ACTION_GET_CONTENT. Test on both configurations.
Summary
Use ActivityResultContracts.PickVisualMedia for Android 13+ Photo Picker (no permissions needed)
Use ActivityResultContracts.GetContent for broader compatibility (Android 4.4+)
Copy selected files to app storage for persistent access — picker URIs are temporary
Use GetMultipleContents or PickMultipleVisualMedia for multi-image selection
In Jetpack Compose, use rememberLauncherForActivityResult() to register pickers
Avoid the deprecated startActivityForResult / onActivityResult pattern