How to Build a Photo Viewing Application in Android using Jetpack Compose?
Gallery App is one of the most used Applications which comes pre-installed on many Android devices and there are several different applications that are present in Google Play to view the media files present in a device. In this article, we will be building a simple Gallery Application for Android using Jetpack Compose. In this application, we will be displaying the photos present on the user’s device and the user will be able to open a single photo from the list of photos.
Note: If you are seeking Java code for Jetpack Compose, please note that Jetpack Compose is only available in Kotlin. It uses features such as coroutines, and the handling of @Composable annotations is handled by a Kotlin compiler. There is no method for Java to access these. Therefore, you cannot use Jetpack Compose if your project does not support Kotlin.
Step by Step Implementation
Step 1: Create a New Project in Android Studio
To create a new project in Android Studio please refer to How to Create/Start a New Project in Android Studio. While choosing the template, select Empty Compose Activity. If you do not find this template, try upgrading the Android Studio to the latest version. We demonstrated the application in Kotlin, so make sure you select Kotlin as the primary language while creating a New Project.
Step 2: Add the Dependency in build.gradle File
Navigate to the app > Gradle Scripts > build.gradle(:app) and add the below dependency to it. We are using Coil for loading images from paths in our Image view.
implementation("io.coil-kt:coil-compose:2.0.0-rc01")
Now sync your project and we will move towards adding permissions in our AndroidManifest.xml file.
Step 3: Adding Permissions in our AndroidManifest.xml File
Navigate to the app > AndroidManifest.xml file and add the below permissions to it.
XML
<!-- permissions for reading external storage --> < uses-permission android:name = "android.permission.READ_EXTERNAL_STORAGE" /> |
Step 4: Creating a New Activity for Displaying a Single Image
Navigate to the app > java > your app’s package name > Right-click on it > New >Compose> Empty Compose Activity and name your activity as MainActivity2 and create a new activity. We will be using this activity to display our single image from the list of different images.
Step 5: Working with the MainActivity.kt File
Go to the MainActivity.kt file and refer to the following code. Below is the code for the MainActivity.kt file. Comments are added inside the code to understand the code in more detail.
Kotlin
import android.Manifest.permission.READ_EXTERNAL_STORAGE import android.app.Activity import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.pm.PackageManager import android.database.Cursor import android.os.Bundle import android.os.Environment import android.provider.MediaStore import android.util.Log import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.GridCells import androidx.compose.foundation.lazy.LazyVerticalGrid import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.Role.Companion.Image import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.* import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.localbroadcastmanager.content.LocalBroadcastManager import coil.compose.rememberImagePainter import com.example.newcanaryproject.ui.theme.* import java.io.File class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super .onCreate(savedInstanceState) setContent { NewCanaryProjectTheme { // on below line we are specifying // background color for our application Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background ) { // on below line we are specifying theme as scaffold. Scaffold( // in scaffold we are specifying top bar. topBar = { // inside top bar we are specifying background color. TopAppBar(backgroundColor = greenColor, // along with that we are specifying title for our top bar. title = { // in the top bar we are specifying tile as a text Text( // on below line we are specifying text // to display in top app bar. text = "Gallery App" , // on below line we are specifying modifier // to fill max width. modifier = Modifier.fillMaxWidth(), // on below line we are specifying text alignment. textAlign = TextAlign.Center, // on below line we are specifying color for our text. color = Color.White ) }) }) { // on below line we are calling custom list // view function to display custom listview. com.example.newcanaryproject.requestPermissions(LocalContext.current, this ) gridView(LocalContext.current) } } } } } override fun onRequestPermissionsResult( requestCode: Int, permissions: Array<out String>, grantResults: IntArray ) { super .onRequestPermissionsResult(requestCode, permissions, grantResults) // this method is called after permissions has been granted. when (requestCode) { 101 -> // in this case we are checking if the permissions are accepted or not. if (grantResults.size > 0 ) { val storageAccepted = grantResults[ 0 ] === PackageManager.PERMISSION_GRANTED if (storageAccepted) { // if the permissions are accepted we are displaying a toast message // and calling a method to get image path. Toast.makeText( this , "Permissions Granted.." , Toast.LENGTH_SHORT).show() getImagePath( this ) } else { // if permissions are denied we are closing the app and displaying the toast message. Toast.makeText( this , "Permissions denied, Permissions are required to use the app.." , Toast.LENGTH_SHORT ).show() } } } } } private fun checkPermission(ctx: Context): Boolean { // in this method we are checking if the permissions are granted or not and returning the result. val result = ContextCompat.checkSelfPermission(ctx, READ_EXTERNAL_STORAGE) return result == PackageManager.PERMISSION_GRANTED } private fun requestPermissions(ctx: Context, activity: Activity) { if (checkPermission(ctx)) { // if the permissions are already granted we are calling // a method to get all images from our external storage. Toast.makeText(ctx, "Permissions granted.." , Toast.LENGTH_SHORT).show() getImagePath(ctx) } else { // if the permissions are not granted we are // requesting permissions on below line ActivityCompat.requestPermissions( activity, arrayOf(android.Manifest.permission.READ_EXTERNAL_STORAGE), 101 ) } } private fun getImagePath(ctx: Context): MutableList<String> { var imgList: MutableList<String> = ArrayList() // in this method we are adding all our image paths // in our aerialist which we have created. // on below line we are checking if the device is having an sd card or not. val isSDPresent = Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED if (isSDPresent) { // if the sd card is present we are creating a new list in // which we are getting our images data with their ids. val columns = arrayOf(MediaStore.Images.Media.DATA, MediaStore.Images.Media._ID) // on below line we are creating a new // string to order our images by string. val orderBy = MediaStore.Images.Media._ID // this method will stores all the images // from the gallery in Cursor val cursor: Cursor? = ctx.contentResolver.query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, columns, null , null , orderBy ) // below line is to get total number of images val count: Int = cursor!!.getCount() // on below line we are running a loop to add // the image file path in our array list. for (i in 0 until count) { // we will be only displaying 10 images // on below line we are breaking the loop // when array list is > 10 if (imgList.size > 8 ) { break } // on below line we are moving our cursor position cursor!!.moveToPosition(i * 3 ) // on below line we are getting image file path val dataColumnIndex: Int = cursor.getColumnIndex(MediaStore.Images.Media.DATA) // after that we are getting the image file path // and adding that path in our array list. imgList.add(cursor.getString(dataColumnIndex)) } // after adding the data to our // array list we are closing our cursor. cursor!!.close() } // on below lien we are returning our list return imgList } // on below line we are creating grid view function for loading our grid view. @OptIn (ExperimentalFoundationApi:: class , ExperimentalMaterialApi:: class ) @Composable fun gridView(context: Context) { // on below line we are creating and initializing our array list var imgList: MutableList<String> = ArrayList() // on below line we are getting out list imgList = getImagePath(context) // on below line we are adding lazy // vertical grid for creating a grid view. LazyVerticalGrid( // on below line we are setting the // column count for our grid view. cells = GridCells.Fixed( 3 ), // on below line we are adding padding // from all sides to our grid view. modifier = Modifier.padding( 10 .dp) ) { // on below line we are displaying our // items up to the size of the list. items(imgList.size) { // on below line we are creating a // card for each item of our grid view. Card( // inside our grid view on below line we are // adding on click for each item of our grid view. onClick = { // on below line we are opening a new activity to display each activity. val i = Intent(context, MainActivity2:: class .java) // on below line we are passing our image to new activity. i.putExtra( "img" , imgList[it]) // on below line we are opening new activity context.startActivity(i) }, // on below line we are adding padding from our all sides. modifier = Modifier .padding( 3 .dp) .width( 100 .dp) .height( 100 .dp), // on below line we are adding elevation for the card. elevation = 2 .dp ) { // on below line we are creating a column on below sides. Column( // on below line we are adding padding // padding for our column and filling the max size. Modifier .fillMaxSize() .fillMaxHeight() .fillMaxWidth(), // on below line we are adding // horizontal alignment for our column horizontalAlignment = Alignment.CenterHorizontally, // on below line we are adding // vertical arrangement for our column verticalArrangement = Arrangement.Center ) { // on below line we are creating an image file. val imgFile = File(imgList[it]) // on below line we are creating image for our grid view item. Image( // on below line we are specifying the drawable image for our image. // painter = painterResource(id = courseList[it].languageImg), painter = rememberImagePainter(data = imgFile), // on below line we are specifying // content description for our image contentDescription = "Javascript" , // on below line we are setting height // and width for our image. modifier = Modifier .fillMaxWidth() .fillMaxHeight() ) } } } } } |
Step 6: Working with the MainActivity2.kt File
Go to the MainActivity2.kt file and refer to the following code. Below is the code for the MainActivity2.kt file. Comments are added inside the code to understand the code in more detail.
Kotlin
import android.content.Context import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextAlign import coil.compose.rememberImagePainter import com.example.newcanaryproject.ui.theme.NewCanaryProjectTheme import com.example.newcanaryproject.ui.theme.greenColor import java.io.File class MainActivity2 : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super .onCreate(savedInstanceState) setContent { NewCanaryProjectTheme { // on below line we are specifying background color for our application Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background ) { // on below line we are specifying theme as scaffold. Scaffold( // in scaffold we are specifying top bar. topBar = { // inside top bar we are specifying background color. TopAppBar(backgroundColor = greenColor, // along with that we are specifying title for our top bar. title = { // in the top bar we are specifying tile as a text Text( // on below line we are specifying text to display in top app bar. text = "Gallery App" , // on below line we are specifying modifier to fill max width. modifier = Modifier.fillMaxWidth(), // on below line we are specifying text alignment. textAlign = TextAlign.Center, // on below line we are specifying color for our text. color = Color.White ) } ) } ) { var img = intent.getStringExtra( "img" ) // on below line we are calling get data and passing email and shared preferences to it. displayImage(LocalContext.current, img) } } } } } } @Composable fun displayImage(ctx: Context, img: String?) { // on below line we are creating a column Column( // inside the column we are adding modifier and specifying max height, max width and max size. modifier = Modifier .fillMaxWidth() .fillMaxHeight() .fillMaxSize(), // on below line we are specifying vertical arrangement for our column. verticalArrangement = Arrangement.Center, // on below line we are specifying horizontal alignment for our column. horizontalAlignment = Alignment.CenterHorizontally ) { val imgFile = File(img) // on below line we are creating image for our grid view item. Image( // on below line we are specifying the drawable image for our image. // painter = painterResource(id = courseList[it].languageImg), painter = rememberImagePainter(data = imgFile), // on below line we are specifying // content description for our image contentDescription = "Javascript" , // on below line we are setting height // and width for our image. modifier = Modifier .fillMaxWidth() .fillMaxHeight() ) } } |
Output: