JETPACK COMPOSE

Easy Image Picking: No Permissions Required using Jetpack Compose

Learn to easily select, store, and load images locally in Android apps without the hassle of permission handling.

Oussama Cherfaoui

--

Photo by Joanna Kosinska on Unsplash

As an Android developer, I know how frustrating it can be to handle permissions when implementing local image picking in an app. That’s why I want to share with you a simplified approach to local image picking using Kotlin and Jetpack Compose. In this tutorial, I’ll show you how to select an image, store its URI in a local database, and load it the next time the user opens the app, all without the hassle of permission handling. Let’s get started!

Prerequisites

  • Knowledge of Kotlin programming language.
  • Understanding of Jetpack Compose fundamentals.
  • Familiarity with Android development using Android Studio.

Creating the Image Picker composable

First, you need to add the coil dependency to load images using the URI (you can use any other library you’re comfortable with) and sync your project to ensure the dependency is successfully added.

implementation("io.coil-kt:coil-compose:2.4.0")

In the provided code, we have defined the ImagePicker composable function, which is responsible for displaying the user interface elements and handling the image picking logic.

First, we declare a mutable state variable imageUri to hold the selected imageUri.

Next, we define the launcher using rememberLauncherForActivityResult. This launcher is responsible for launching the image picker activity and receiving the result. We use the ActivityResultContracts.OpenDocument() contract, which allows the user to pick a document (in this case, an image file) from the device's storage. When the user selects an image, the lambda function passed to the launcher is invoked, and we assign the selected uri to the imageUri variable.

Then we’re defining a simple UI to display the loaded image using coil, the AsyncImage composable is used to display the selected image. We pass the imageUri as the model parameter, which automatically loads and displays the image when the imageUri changes. The contentDescription is set to null for simplicity.

Below the image, we have a Button that triggers the image picking process. When clicked, the launcher is launched by calling launcher.launch(arrayOf("image/*")). This opens the image picker activity, allowing the user to select an image from their device's gallery.

Implementing Local Storage for Image URIs

For simplicity we will use datastore for quick store and load of the image URI, but you can use Room or any local storage approach you’re comfortable with. To use datastore you need add the following dependency:

implementation 'androidx.datastore:datastore-preferences:1.0.0'

Then we will create a class that handles the storage and retrieval of the image URI:

First, we declare a property storeData of type DataStore<Preferences> using the preferencesDataStore delegate. This property is responsible for accessing the DataStore instance associated with the provided name ("data"). The DataStore allows us to store key-value pairs persistently.

Next, we have a suspend function storeImage that takes a Context and a value (the image URI) as parameters. Inside the function, we use the edit function on the storeData to modify the preferences. We access the preferences using the preferences parameter and store the image URI using the key "image".

Similarly, we have another suspend function getImage that takes a Context as a parameter. This function returns a Flow<String?>, which is a stream of nullable strings. Inside the function, we retrieve the image URI from the storeData using the data property. We then map the preferences to extract the image URI value associated with the "image" key.

By using this StoreData class, you can easily store and retrieve the image URI in your local storage.

Integrating Storage with the Image Picker

In the provided code, we have made modifications and additions to the ImagePicker composable to integrate it with the storage functionality. Let's go through the changes and understand their purpose.

  1. We’re getting access to the current context within the composable.
  2. We create an instance of the StoreData class using remember { StoreData() }. This initializes the storage class, allowing us to store and retrieve image URIs.
  3. We create a coroutine scope that is required for launching coroutines.
  4. When the user selects an image (that is not null), we must call takePersistableUriPermission to grant the necessary read permission to the image URI.
  5. Then, we launch a coroutine using scope.launch to store the image URI in the dataStore using the storeImage function.
  6. We use LaunchedEffect that will be launched only once in the initial composition.
  7. Inside the LaunchEffect, we collect the flow of the image URI from the dataStore using getImage(context).collect. If the retrieved URI is not null, we parse it into a Uri object and assign it to imageUri.

By implementing these modifications and additions, the ImagePicker composable now integrates with the storage functionality. It allows users to pick an image, automatically saves the selected image URI in storage, and loads the image URI when the app is reopened, providing a seamless experience for handling images in your Android app.

Drawback

One drawback of storing the image URI without making a local copy of the image itself is that if the original image is deleted or moved from its original location, the stored URI will no longer be valid. This can lead to broken image links and potential issues when trying to load the image in the future.

Wrap-up

In summary, we have seen how Kotlin and Jetpack Compose can be used to effortlessly handle local image picking in an Android app. By integrating a user-friendly image picker and a storage solution, we eliminate the need for complex permission handling while enabling seamless storage and retrieval of image URIs. This approach simplifies the development process and enhances the user experience, making it easier to create engaging apps that seamlessly handle local image picking.

--

--