A Guide to Enhancing User Experience by Integrating Image Picker Functionality in Your Android App
Alice, our intrepid explorer of Android Wonderland, had a grand vision for her Vaxin app. She dreamed of allowing users to personalize their experience by choosing an image from their phone’s gallery. Little did she know the adventures that awaited her.
Part 1: Alice’s Journey in the Android Wonderland
With unwavering determination, Alice declared a state variable named imageURI
to hold the URI of the chosen image. She was well-prepared and cleverly used the Elvis operator to provide a default image just in case. She had learned from her previous adventures in Wonderland that it’s wise to be prepared for the unexpected.
val imageURI = remember {
mutableStateOf<String>(
child.imageURI?.toString() ?: "android.resource://${context.packageName}/drawable/child"
)
}
Suddenly, out of thin air, the enigmatic Cheshire Cat materialized before Alice.
“Would you tell me, please, which way I ought to go from here?” Alice enquired.
“The depends a good deal on which image you want to show,” said the Cat.
“I don’t much care which one –” said Alice.
“Then it doesn’t matter which one you show,” said the Cat.
“– so long as the user picks one,” added Alice as an explanation.
“Then be sure to look in the Coil library,” said the Cat, “for a looking glass into the content that your URI will find as surely as White Rabbit has fur and whiskers.”
With that cryptic message, the Cheshire Cat vanished into thin air once again, leaving Alice to tease out the meaning of his words.
Alice continued her journey in the direction the Cheshire Cat pointed out. She created a magical portal using Coil’s AsyncImage
composable to display the selected image. The image was not just displayed; it was crafted into a neat circular shape and cropped to perfection to avoid any distortion. Alice marked it as clickable, adding a “TODO” to remind herself to complete this part of her adventure.
AsyncImage(
model = imageURI.value,
contentDescription = "Generic Child",
contentScale = ContentScale.Crop,
modifier = Modifier
.padding(5.dp)
.size(75.dp)
.clip(shape = CircleShape)
.clickable {
TODO("Lauch Image Picker")
}
)
Just then, she noticed the Queen of Hearts was behind her, observing her code. With a stern tone, she questioned Alice’s action.
“Who gave you permission to use the image gallery in your app? Off with her head!“
“Don’t be silly,” said Alice. But she had turned thoughtful, looking for a way to launch the image gallery from within the app and obtain a URI to the selected image.
Part 2: The Enchanted Image Picker
As Alice contemplated her next move, the White Rabbit hurried by, nervously clutching his pocket watch and casting furtive glances at the time ticking by.
“Alice, you must use rememberLauncherForActivityResult
. It’s like a magical gate that leads to a world of images. When the user picks an image, the launcher will provide you with the image’s URI.”
He took the pocket watch out of his waistcoat and remarking on how late he was, scurried off.
Alice followed the White Rabbit’s guidance and set up the launcher, ready to open the image picker.
val singlePhotoPickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickVisualMedia(),
onResult = { uri ->
Log.d(TAG, "Got content uri: ${uri}")
uri?.let {
imageURI.value = it.toString() // For immediate preview
}
}
)
With the URI in her grasp, Alice displayed the image on the screen using AsyncImage
, inserting singlePhotoPickerLauncher
into the callback of the clickable
event, much like hanging a beautiful painting on the wall of her app.
AsyncImage(
model = imageURI.value,
contentDescription = "Generic Child",
contentScale = ContentScale.Crop,
modifier = Modifier
.padding(5.dp)
.size(75.dp)
.clip(shape = CircleShape)
.clickable {
singlePhotoPickerLauncher.launch(
PickVisualMediaRequest(
ActivityResultContracts.PickVisualMedia.ImageOnly
)
)
}
)
As she marveled at her creation, a curious apparition appeared in the air. It puzzled her at first, but she soon recognized it as the grinning Cheshire Cat. Alice was delighted to have someone review her code.
“How are you getting on?” said the Cat as soon as its mouth was big enough to speak.
Alice waited patiently until the eyes appeared and then nodded. “It’s no use speaking to it,” she thought, “until its ears have come, or at least one of them.”
In another minute, the whole head appeared, and Alice put down her keyboard to share her coding adventure. The Cat seemed satisfied with what it had seen and started fading just as miraculously as she had appeared.
But before vanishing completely, the Cheshire Cat left Alice with a valuable piece of advice.
“Beware, Alice, there may be villains who delete images from the gallery. There are 4 types of URI and they are not all the same. The URI from the image picker is a content URI. Always remember to copy the content to your app’s local storage and obtain a file URI that you persist to the app’s database.”
Alice felt a pang of unease. “What if the Queen of Hearts has already deleted the image from the gallery?” she wondered.
Determined to secure her chosen image, Alice wrote a function to read the content from the content URI into a byte array and then write the byte array to a file. This process gave her the precious file URI she sought.
fun getFileURI(contentUri: Uri, childId: String, context: Context, contentResolver: ContentResolver): Uri {
/* Read contents at content URI into byte array */
val imBytes = contentResolver.openInputStream(contentUri)?.use {
it.readBytes()
}
/* Write byte array into a local file with appropriate name */
val file = File(context.filesDir, "${childId}.jpeg")
FileOutputStream(file)?.use {
it.write(imBytes)
}
/* Get the file URI to persistently access image */
return file.toUri()
}
With the URI secured, Alice was ready to ship it to the View Model for persistence in the Room database. She expanded the launcher to do just that, wrapping the URI in data class OnAddChildPhoto
of sealed class AddChildEvent
for handling all user-originated events from that screen. She used the View Model’s onEvent
method for the transfer.
val singlePhotoPickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickVisualMedia(),
onResult = { uri ->
Log.d(TAG, "Got content uri: ${uri}")
uri?.let {
imageURI.value = it.toString() // For immediate preview
val fileURI = getFileURI(it, child.childName, context, contentResolver)
Log.d(TAG, "Got file URI: ${fileURI}")
onEvent(AddChildEvent.OnAddChildPhoto(
imageURI = fileURI,
child = child
))
}
}
)
At long last, it was time to reveal the image picker feature.