Android
Image Gallery
Pick Images
Android Development
Mobile App Development

android pick images from gallery

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

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+)

kotlin
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)

kotlin
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

kotlin
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

kotlin
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)

kotlin
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

Course illustration
Course illustration

All Rights Reserved.