develop android image galery app using kotlin source code

How to Develop Android Image Gallery App using Kotlin – Tutorial with Complete Source Code

In this tutorial, we will develop a complete Android Photo Gallery application in Kotlin. I will share the source code of my gallery application from splash screen till the end.

we will also cover following topics in Kotlin.

  • How to implement Singleton pattern in Kotlin.
  • How to pass a class object from one activity to another using Parcelable in Kotlin
  • Shared preferences Kotlin example.
  • Recyclerview in Kotlin.
  • Setup RecyclerView Adapter in Kotlin.
  • NavigationView and drawer layout setup in Kotlin.
  • Load and display images from internal storage in Android using Kotlin.
  • we will also use Glide image loading library.
  • Android Runtime permissions.

If you are a beginner and you are here to learn Kotlin programming languageI recommend you go through these tutorials first.

  1. Kotlin idioms Tutorial
  2. classes Objects Modifiers and Interfaces in Kotlin Tutorial

So you will learn a lot about Kotlin if you develop this simple photo gallery application in Android using Kotlin programming language.

Let’s Start Development.

Create a new project in Android Studio and enable check “Add support for Kotlin” while creating a new project.

Add Required permissions in the Manifest file.

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

Create Data Class “Albums” and implement Parcelable interface.

This is our Model class which has information about all folders like Folder Name (Whatsapp Images, Camera), image path of the most recent image in a folder, total number of images in a folder.

As we have to pass this information from splash screen to our MainActivity using Intent. so “Albums” data class will implement Parcelable Interface.

data class Albums(var folderNames: String, var imagePath: String, var imgCount: Int, var isVideo: Boolean) : Parcelable {
    constructor(parcel: Parcel) : this(
            parcel.readString(),
            parcel.readString(),
            parcel.readInt(),
            parcel.readByte() != 0.toByte()) {
    }

    override fun writeToParcel(parcel: Parcel, flags: Int) {
        parcel.writeString(folderNames)
        parcel.writeString(imagePath)
        parcel.writeInt(imgCount)
        parcel.writeByte(if (isVideo) 1 else 0)
    }

    override fun describeContents(): Int {
        return 0
    }

    companion object CREATOR : Parcelable.Creator<Albums> {
        override fun createFromParcel(parcel: Parcel): Albums {
            return Albums(parcel)
        }

        override fun newArray(size: Int): Array<Albums?> {
            return arrayOfNulls(size)
        }
    }
}

Add new Splash Activity and add below code.

In the onCreate method of our Splash Activity first of all we need access to external storage and for that, we have to ask a user to permit our application to access images stored on a device.

After a user has granted read_external_storage permission.

We can load images from storage and send image name, path and the total number of images in a folder to our Main Activity for showing all folders and their most recent image in Recyclerview.

// SplashActivity.kt

internal var SPLASH_TIME_OUT = 800

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    setContentView(R.layout.activity_splash)

    Handler().postDelayed(
            {
                 // check if user has grannted permission to access device external storage.
                 // if not ask user for access to external storage.
                if (!checkSelfPermission()) {
                    requestPermission()
                } else {
                   // if permission granted read images from storage.
                   //  source code for this function can be found below.
                    loadAllImages()
                }
            }, SPLASH_TIME_OUT.toLong())
}

Add Run time permission handling code

private fun requestPermission() {
    ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), 6036)
}


private fun checkSelfPermission(): Boolean {

    if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
        return false
    } else
        return true
}

If a user has granted or denied permission this method will be called.

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
    when (requestCode) {
        6036 -> {
            if (grantResults.size > 0) {
                var permissionGranted = grantResults[0] == PackageManager.PERMISSION_GRANTED
                if (permissionGranted) {

    // Now we are ready to access device storage and read images stored on device.

                    loadAllImages()
                } else {
                    Toast.makeText(this, "Permission Denied! Cannot load images.", Toast.LENGTH_SHORT).show()
                }
            }
        }
    }
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}

if permission granted we can load images from internal storage and pass Images to MainActivity.kt

Pass Data Class (model class) object in intent as Parcelable.

This function will call another method getAllShownImagesPath(context) which will return List of type Albums.

And pass the returning Albums list to MainActivity via Intent.

MainActivity will show a list of all folders which has images and videos for example (Camera, Instagram, Whatsapp Images) in RecyclerView.

fun loadAllImages() {
    var imagesList = getAllShownImagesPath(this)
    var intent = Intent(this, MainActivity::class.java)
    intent.putParcelableArrayListExtra("image_url_data", imagesList)
    startActivity(intent)
    finish()
}

In above code we have object of Data Class and we need to pass this data to another activity so we are passing as serialized data.

Loading images from Internal storage

In this code snippet, we are reading all folders which has images and videos using MediaStore class.

And returning result as ArrayList<Albums>.

private fun getAllShownImagesPath(activity: Activity): ArrayList<Albums> {

    val uri: Uri
    val cursor: Cursor
    var cursorBucket: Cursor
    val column_index_data: Int
    val column_index_folder_name: Int
    val listOfAllImages = ArrayList<String>()
    var absolutePathOfImage: String? = null
    var albumsList = ArrayList<Albums>()
    var album: Albums? = null


    val BUCKET_GROUP_BY = "1) GROUP BY 1,(2"
    val BUCKET_ORDER_BY = "MAX(datetaken) DESC"

    uri = android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI

    val projection = arrayOf(MediaStore.Images.ImageColumns.BUCKET_ID,
            MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME,
            MediaStore.Images.ImageColumns.DATE_TAKEN,
            MediaStore.Images.ImageColumns.DATA)

    cursor = activity.contentResolver.query(uri, projection, BUCKET_GROUP_BY, null, BUCKET_ORDER_BY)

    if (cursor != null) {
        column_index_data = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)
        column_index_folder_name = cursor
                .getColumnIndexOrThrow(MediaStore.Images.Media.BUCKET_DISPLAY_NAME)
        while (cursor.moveToNext()) {
            absolutePathOfImage = cursor.getString(column_index_data)
            Log.d("title_apps", "bucket name:" + cursor.getString(column_index_data))

            val selectionArgs = arrayOf("%" + cursor.getString(column_index_folder_name) + "%")
            val selection = MediaStore.Images.Media.DATA + " like ? "
            val projectionOnlyBucket = arrayOf(MediaStore.MediaColumns.DATA, MediaStore.Images.Media.BUCKET_DISPLAY_NAME)

            cursorBucket = activity.contentResolver.query(uri, projectionOnlyBucket, selection, selectionArgs, null)
            Log.d("title_apps", "bucket size:" + cursorBucket.count)

            if (absolutePathOfImage != "" && absolutePathOfImage != null) {
                listOfAllImages.add(absolutePathOfImage)
                albumsList.add(Albums(cursor.getString(column_index_folder_name), absolutePathOfImage, cursorBucket.count, false))
            }
        }
    }
    return getListOfVideoFolders(albumsList)
}

// This function is resposible to read all videos from all folders.
private fun getListOfVideoFolders(albumsList: ArrayList<Albums>): ArrayList<Albums> {

    var cursor: Cursor
    var cursorBucket: Cursor
    var uri: Uri
    val BUCKET_GROUP_BY = "1) GROUP BY 1,(2"
    val BUCKET_ORDER_BY = "MAX(datetaken) DESC"
    val column_index_album_name: Int
    val column_index_album_video: Int

    uri = android.provider.MediaStore.Video.Media.EXTERNAL_CONTENT_URI

    val projection1 = arrayOf(MediaStore.Video.VideoColumns.BUCKET_ID,
            MediaStore.Video.VideoColumns.BUCKET_DISPLAY_NAME,
            MediaStore.Video.VideoColumns.DATE_TAKEN,
            MediaStore.Video.VideoColumns.DATA)

    cursor = this.contentResolver.query(uri, projection1, BUCKET_GROUP_BY, null, BUCKET_ORDER_BY)

    if (cursor != null) {
        column_index_album_name = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.BUCKET_DISPLAY_NAME)
        column_index_album_video = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA)
        while (cursor.moveToNext()) {
            Log.d("title_apps", "bucket video:" + cursor.getString(column_index_album_name))
            Log.d("title_apps", "bucket video:" + cursor.getString(column_index_album_video))
            val selectionArgs = arrayOf("%" + cursor.getString(column_index_album_name) + "%")

            val selection = MediaStore.Video.Media.DATA + " like ? "
            val projectionOnlyBucket = arrayOf(MediaStore.MediaColumns.DATA, MediaStore.Video.Media.BUCKET_DISPLAY_NAME)

            cursorBucket = this.contentResolver.query(uri, projectionOnlyBucket, selection, selectionArgs, null)
            Log.d("title_apps", "bucket size:" + cursorBucket.count)

            albumsList.add(Albums(cursor.getString(column_index_album_name), cursor.getString(column_index_album_video), cursorBucket.count, true))
        }
    }
    return albumsList
}

What we have done so far

We have learned What we need to do for developing a Image Gallery Application in Android and for that purpose we have

  1. Added Request for Run time permissions for loading and displaying images in our gallery application.
  2. We are done with Loading images path from Android device internal storage using MediaStore and Cursor.
  3. How to pass Serialized Data Class object from one Activity to another in Kotlin.
  4. Learned how to use Data Class in Kotlin (Albums.kt) in our project.

Now create another activity and name it MainActivity.kt class in your project.

What we will do in MainActivity

  1. We will receive Albums object sent from Splash Activity.
  2. Write code for RecyclerView item click listener in Kotlin, for that purpose we will use Interface call back approach.
  3. Create layout file and RecyclerView adapter Class for displaying a list of all folders.

Create layout files for MainActivity.kt

// activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    <android.support.v7.widget.Toolbar
        android:id="@+id/my_toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        android:elevation="4dp"
        app:titleTextColor="@color/colorIcons" />
    
    <android.support.v4.widget.DrawerLayout
        android:id="@+id/drawer_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@+id/my_toolbar"
        android:fitsSystemWindows="true">

        <!-- Your contents -->
        <include layout="@layout/include_main_content"></include>

        <android.support.design.widget.NavigationView
            android:id="@+id/navigation"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_gravity="start"
            android:onClick="toast"
            app:menu="@menu/my_navigation_menu"
            app:theme="@style/NavigationDrawerStyle" />
    </android.support.v4.widget.DrawerLayout>
</RelativeLayout>

//include_main_content.xml 

<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/main_content"
    android:layout_width="match_parent"
    android:layout_height="match_parent">


    <android.support.v7.widget.RecyclerView
        android:id="@+id/rvAlbums"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clipToPadding="false" />

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab_camera"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|right"
        android:layout_margin="16dp"
        android:src="@drawable/ic_photo_camera_white_24dp"
        app:layout_anchor="@id/rvAlbums"
        app:layout_anchorGravity="bottom|right|end"
        app:layout_behavior="com.title_apps.canalpic.util.ScrollAwareFABBehavior" />

</android.support.design.widget.CoordinatorLayout>
//MainActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    savedState = savedInstanceState

    if (savedState != null)
        folder_name = savedInstanceState!!.getString("folder_name")

    setSupportActionBar(my_toolbar)
    // Enable the Up button
    supportActionBar!!.setDisplayHomeAsUpEnabled(true)
    supportActionBar!!.setHomeAsUpIndicator(resources.getDrawable(R.drawable.ic_menu_white_24dp))

    setupNavigationView()

    var extra = intent.extras;
    if (extra != null) {
        var extraData = extra.get("image_url_data") as ArrayList<Albums>
        select_fragment(extraData)
    }

    drawer_layout_listener()
    supportActionBar!!.setTitle("Folders")
}
// here we are sending folder name on which user has clicked, for example if user has
// clicked on downloads folder from list of all folders in our gallery app we will pass that
// folder name to our next activity which will load and display all images from that folder.

override fun onItemClick(position: String, isVideo: Boolean) {

    var bundle = Bundle()
    bundle.putString("folder_name", position)
        var intent = Intent(this, AlbumActivity::class.java)
        intent.putExtra("folder_name", position)
        startActivity(intent)
}

Initialize Glide and Setup RecyclerView for displaying images in our Android Gallery Application.

private var folder_name: String = ""

public fun select_fragment(imagesList: ArrayList<Albums>) {

    val options = RequestOptions()
            .diskCacheStrategy(DiskCacheStrategy.RESOURCE).override(160, 160).skipMemoryCache(true).error(R.drawable.ic_image_unavailable)
    val glide = Glide.with(this)

    val builder = glide.asBitmap()
    rvAlbums?.layoutManager = GridLayoutManager(this, 2)

    rvAlbums?.setHasFixedSize(true)

    // AlbumFoldersAdapter.kt is RecyclerView Adapter class. we will implement shortly.
    rvAlbums?.adapter = AlbumFoldersAdapter(imagesList, this, options, builder, glide, this)


    rvAlbums?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
        override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
            super.onScrolled(recyclerView, dx, dy)
        }

        override fun onScrollStateChanged(recyclerView: RecyclerView?, newState: Int) {
            super.onScrollStateChanged(recyclerView, newState)
            when (newState) {
                RecyclerView.SCROLL_STATE_IDLE -> glide.resumeRequests()
                AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL, AbsListView.OnScrollListener.SCROLL_STATE_FLING -> glide.pauseRequests()
            }
        }
    }
    )

    fab_camera?.setOnClickListener(object : View.OnClickListener {
        override fun onClick(p0: View?) {
            launchCamera()
        }
    }
    )
}

NavigationView and drawerLayout click listeners in Kotlin code.

// drawer layout click listener in Kotlin source code.
private fun drawer_layout_listener() {

    drawer_layout.addDrawerListener(object : DrawerLayout.DrawerListener {
        override fun onDrawerStateChanged(newState: Int) {
        }

        override fun onDrawerSlide(drawerView: View?, slideOffset: Float) {
        }

        override fun onDrawerClosed(drawerView: View?) {
            supportActionBar!!.setHomeAsUpIndicator(resources.getDrawable(R.drawable.ic_menu_white_24dp))
        }

        override fun onDrawerOpened(drawerView: View?) {
            supportActionBar!!.setHomeAsUpIndicator(resources.getDrawable(R.drawable.ic_keyboard_backspace_white_24dp))
        }
    }
    )
}

// Navigation item click listener Kotlin source code.
private fun setupNavigationView() {

    navigation.setNavigationItemSelectedListener(object : NavigationView.OnNavigationItemSelectedListener {
        override fun onNavigationItemSelected(item: MenuItem): Boolean {
            drawer_layout.closeDrawer(Gravity.START)
            when (item.itemId) {
                R.id.nav_all_folders -> {
                                 }
                R.id.nav_hidden_folders -> {
                                 }
            }
            return false
        }
    })
}

Now we will add code for AlbumsFolderAdapter.kt for our RecyclerView.

Create layout files for MainActivity RecyclerView Adapter

// list_layout.xml
// layout file for RecyclerView adapter.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <android.support.v7.widget.CardView
        android:id="@+id/card_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_margin="@dimen/five_dp"
        android:elevation="3dp"
        card_view:cardCornerRadius="@dimen/zero_dp">

        <RelativeLayout
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:background="?attr/selectableItemBackgroundBorderless">

            <ImageView
                android:id="@+id/thumbnail"
                android:layout_width="match_parent"
                android:layout_height="@dimen/album_cover_height"
                android:scaleType="centerCrop" />

            <TextView
                android:id="@+id/title"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_below="@id/thumbnail"
                android:paddingLeft="@dimen/ten_dp"
                android:paddingRight="@dimen/ten_dp"
                android:paddingTop="@dimen/ten_dp"
                android:text="Camera"
                android:textColor="@color/colorPrimaryText"
                android:textSize="@dimen/fifteen_dp" />

            <RelativeLayout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_below="@id/title">

                <TextView
                    android:id="@+id/photoCount"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:paddingBottom="@dimen/five_dp"
                    android:paddingLeft="@dimen/ten_dp"
                    android:paddingRight="@dimen/six_dp"
                    android:textColor="@color/colorSecondaryText"
                    android:textSize="@dimen/twelve_dp" />
            </RelativeLayout>
        </RelativeLayout>
    </android.support.v7.widget.CardView>
</LinearLayout>

Now create another class and name it AlbumFoldersAdapter.kt which is our RecyclerView adapter.

class AlbumFoldersAdapter(val albumList: ArrayList<Albums>, val context: Context, val options: RequestOptions, val glide: RequestBuilder<Bitmap>, val glideMain: RequestManager, val inOnItemClick: IOnItemClick) : RecyclerView.Adapter<AlbumFoldersAdapter.ViewHolder>() {

 override fun onViewRecycled(holder: ViewHolder?) {
 if (holder != null) {
 //glideMain.clear(holder.itemView.thumbnail)
 // glide.clear(holder.itemView.thumbnail)
 //Glide.get(context).clearMemory()
 // holder?.itemView?.thumbnail?.setImageBitmap(null)
 }// Glide.clear(holder?.itemView?.thumbnail)
 super.onViewRecycled(holder)

}

override fun onViewDetachedFromWindow(holder: ViewHolder) {
 if (holder != null) {
 // glideMain.clear(holder.itemView.thumbnail)
 //Glide.get(context).clearMemory()
 // holder?.itemView?.thumbnail?.setImageBitmap(null)
 
}

super.onViewDetachedFromWindow(holder)
 }

override fun getItemCount(): Int {
 return albumList.size
 }

override fun onBindViewHolder(holder: ViewHolder?, position: Int) {
 holder?.bindItems(albumList.get(position), glide, options, inOnItemClick, albumList.get(position).isVideo)

holder?.itemView?.title?.setText(albumList.get(position).folderNames)
 if (albumList.get(position).isVideo)
 holder?.itemView?.photoCount?.setText("" + albumList.get(position).imgCount + " videos")
 else
 holder?.itemView?.photoCount?.setText("" + albumList.get(position).imgCount + " photos")
 }

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
 val v = LayoutInflater.from(parent.context).inflate(R.layout.list_layout, parent, false)
 return ViewHolder(v)
 }

class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

fun bindItems(albumList: Albums, glide: RequestBuilder<Bitmap>, options: RequestOptions, inOnItemClick: IOnItemClick, isVideo: Boolean) {
 glide.load(albumList.imagePath).apply { options }.thumbnail(0.4f)
 .into(itemView.thumbnail)

itemView.setOnClickListener(object : View.OnClickListener {
 override fun onClick(p0: View?) {
 inOnItemClick.onItemClick(albumList.folderNames, isVideo)
     }
  })
}}}

Add Interface for handling RecyclerView item click listener.

interface IOnItemClick {
fun onItemClick(position: String, isVideo: Boolean)
}

We are DONE with displaying all images folders and their most recent image in the particular folder in RecyclerView.

We have also added RecyclerView Adapter code, we have used Glide for loading images in RecyclerView adapter.

Now If user click on Whatsapp Images folder from the list of all folders.

We will create a new Activity which will show a list of images in that particular folder in RecyclerView.

Create new Activity AlbumsActivity.kt which will be responsible for showing images in a single folder in RecyclerView.

Create Layout file for AlbumsActivity.kt

// activity_album.xml

<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/main_content"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v7.widget.Toolbar
        android:id="@+id/my_album_toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        android:elevation="4dp"
        app:titleTextColor="@color/colorIcons" />

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rvAlbumSelected"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginTop="?attr/actionBarSize"
        android:clipToPadding="false" />

</android.support.design.widget.CoordinatorLayout>

AlbumsActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_album)

    setSupportActionBar(my_album_toolbar)
    // Enable the Up button
    supportActionBar!!.setDisplayHomeAsUpEnabled(true)

    val folder_name = intent.getStringExtra("folder_name")
    supportActionBar!!.setTitle("" + folder_name)
    val isVideo = intent.getBooleanExtra("isVideo", false)
    init_ui_views(folder_name, isVideo)

}
var adapter: SingleAlbumAdapter? = null

private fun init_ui_views(folderName: String?, isVideo: Boolean?) {

    val options = RequestOptions()
            .diskCacheStrategy(DiskCacheStrategy.RESOURCE).override(160, 160).skipMemoryCache(true).error(R.drawable.ic_image_unavailable)
    val glide = Glide.with(this)
    val builder = glide.asBitmap()

        rvAlbumSelected.layoutManager = GridLayoutManager(this, 2)
    rvAlbumSelected?.setHasFixedSize(true)
    adapter = SingleAlbumAdapter(getAllShownImagesPath(this, folderName, isVideo), this, options, builder, glide, this)
    rvAlbumSelected?.adapter = adapter

    rvAlbumSelected?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
        override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
            super.onScrolled(recyclerView, dx, dy)
        }

        override fun onScrollStateChanged(recyclerView: RecyclerView?, newState: Int) {
            super.onScrollStateChanged(recyclerView, newState)
            when (newState) {
                RecyclerView.SCROLL_STATE_IDLE -> glide.resumeRequests()
                AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL, AbsListView.OnScrollListener.SCROLL_STATE_FLING -> glide.pauseRequests()
            }
        }
    }
    )
}

// Read all images path from specified directory.

private fun getAllShownImagesPath(activity: Activity, folderName: String?, isVideo: Boolean?): MutableList<String> {

    val uri: Uri
    val cursorBucket: Cursor
    val column_index_data: Int
    val listOfAllImages = ArrayList<String>()
    var absolutePathOfImage: String? = null

    val selectionArgs = arrayOf("%" + folderName + "%")

    uri = android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI
    val selection = MediaStore.Images.Media.DATA + " like ? "

    val projectionOnlyBucket = arrayOf(MediaStore.MediaColumns.DATA, MediaStore.Images.Media.BUCKET_DISPLAY_NAME)

    cursorBucket = activity.contentResolver.query(uri, projectionOnlyBucket, selection, selectionArgs, null)

    column_index_data = cursorBucket.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)

    while (cursorBucket.moveToNext()) {
        absolutePathOfImage = cursorBucket.getString(column_index_data)
        if (absolutePathOfImage != "" && absolutePathOfImage != null)
            listOfAllImages.add(absolutePathOfImage)
    }
    return listOfAllImages.asReversed()
}

Item Click Listener for Album Activity.

override fun onItemClick(position: String, isVideo: Boolean) {
    val intent = Intent(this, PhotoActivity::class.java)
    intent.putExtra("folder_name", position)
    startActivity(intent)
}

RecyclerView adapter for AlbumActivity.kt

class SingleAlbumAdapter(val albumList: MutableList<String>, val context: Context, val options: RequestOptions, val glide: RequestBuilder<Bitmap>, val glideMain: RequestManager, val inOnItemClick: IOnItemClick) : RecyclerView.Adapter<SingleAlbumAdapter.ViewHolder>() {


    override fun onViewRecycled(holder: ViewHolder?) {
        if (holder != null) {
        }
        super.onViewRecycled(holder)
    }

    override fun onViewDetachedFromWindow(holder: ViewHolder) {
        if (holder != null) {

        }
        super.onViewDetachedFromWindow(holder)
    }

    override fun getItemCount(): Int {
        return albumList.size
    }

    override fun onBindViewHolder(holder: ViewHolder?, position: Int) {
        holder?.bindItems(albumList.get(position), glide, options, inOnItemClick)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val v = LayoutInflater.from(parent.context).inflate(R.layout.list_single_album_layout, parent, false)
        return ViewHolder(v)
    }

    class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        fun bindItems(albumList: String, glide: RequestBuilder<Bitmap>, options: RequestOptions, inOnItemClick: IOnItemClick) {

            glide.load(albumList).apply { options }.thumbnail(0.4f)
                    .into(itemView.thumbnail)

            itemView.setOnClickListener(object : View.OnClickListener {
                override fun onClick(p0: View?) {
                    inOnItemClick.onItemClick(albumList, false)
                }
            })
        }
    }
}

Layout File for Single Album Adapter.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <android.support.v7.widget.CardView
        android:id="@+id/card_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_margin="@dimen/five_dp"
        android:elevation="3dp"
        card_view:cardCornerRadius="@dimen/zero_dp">

        <RelativeLayout
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:background="?attr/selectableItemBackgroundBorderless">

            <ImageView
                android:id="@+id/thumbnail"
                android:layout_width="@dimen/album_cover_height"
                android:layout_height="@dimen/album_cover_height"
                android:scaleType="centerCrop" />
        </RelativeLayout>
    </android.support.v7.widget.CardView>
</LinearLayout>

Now Lets add Detail Activity which will show the single image.

// SingleActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_photo)

    setSupportActionBar(toolbar)
    // Enable the Up button
    supportActionBar!!.setDisplayHomeAsUpEnabled(true)
    supportActionBar!!.setDisplayShowTitleEnabled(false)

    val folder_name = intent.getStringExtra("folder_name")
    Glide.with(this).load(folder_name).into(imageFullScreenView)

    Handler().postDelayed(Runnable
    {
        if (supportActionBar != null)
            appbar.animate().translationY(-appbar.bottom.toFloat()).setInterpolator(AccelerateInterpolator()).start()
        isAppBarShown = false
    }, 1500)


}
// activity_photo.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/black"
    tools:context="com.title_apps.canalpic.screens.detail.PhotoActivity">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/appbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#93000000">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:elevation="4dp"
            app:titleTextColor="@color/colorIcons" />
    </android.support.design.widget.AppBarLayout>


    <ImageView
        android:id="@+id/imageFullScreenView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center" />

</android.support.design.widget.CoordinatorLayout>

Conclusion

You have developed complete Image Gallery Android Application and along the way you learned Kotlin syntax, Glide Library, RecyclerView, NavigationView, Drawer Layout and their click listeners in Kotlin,

You have also learned how to create and use Data Class in Kotlin, How to create and use RecyclerView and its Adapter in Kotlin.

 

Recommended Reading :-

Posts created 24

30 thoughts on “How to Develop Android Image Gallery App using Kotlin – Tutorial with Complete Source Code

  1. I like it. One question, I’m not able to see where the ids for the navigation get created. It looks like the @menu/my_navigation_menu xml should be included but isn’t. Can you post that too please?
    Thanks.

  2. good posts, I have two questions:
    1: what does glide.resumeRequests() do?
    2: may your code causes OOM with too many or large pictures?

    1. 1 – As Glide load and process images in background so you can pause or resume background working.
      2- I have paid special attention to Out of memory exception while dealing with images. since we are displaying images in RecyclerView, You may have noticed that I have initialized Glide in activity and not in adapter for every image, we are not loading full sized image in memory for displaying in RecyclerView, Also skip memory cache is set to true so we do not cache every image.
      If you have any suggestion which can help in improving and optimizing this code snippet, you are welcome.

      1. Exactly !
        i tried to do it, but it seems i’m missing a few things… :/
        Is it possible to have the whole project by github or something else?
        Thanks a lot 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *

Related Posts

Begin typing your search term above and press enter to search. Press ESC to cancel.

Back To Top