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 language. I recommend you go through these tutorials first.
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
- Added Request for Run time permissions for loading and displaying images in our gallery application.
- We are done with Loading images path from Android device internal storage using MediaStore and Cursor.
- How to pass Serialized Data Class object from one Activity to another in Kotlin.
- 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
- We will receive Albums object sent from Splash Activity.
- Write code for RecyclerView item click listener in Kotlin, for that purpose we will use Interface call back approach.
- 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 :-
- Building Android MVP in Kotlin using Dagger2 and Retrofit Tutorial
- Top Kotlin Interview Questions
- Kotlin Idioms Tutorial
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.
Hi Dan you just have to create new xml file and place it in menu folder and add required menu items in it.
Thanks, got it. Wasn’t sure if there was something missing.
Thanks again!
Hi, Is it possible to get the Github link of this project?
Hi Jeffrey, I will soon upload source code on github and will let you know.
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 – 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 initializedGlide
in activity and not in adapter for every image, we are not loading full sized image in memory for displaying inRecyclerView
, 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.
请问有github地址吗 ?
Hello,
Can you please provide me your Email_id, @hammadtariq?
I want to communicate regarding Kotlin and Java topic.
hammadtariq.me@gmail.com
Hello hammadtariq!
Great tutorial !
Is it possible to have your github ?
Thanks
Thankyou 🙂 You need source code of this gallery app ?
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 🙂
Okay bro, i am preparing github repo for this project. hopefully will update you in 2,3 days.
Did you uploaded the code to Git?? can you please share the link over here!
Thank you for complimenting 🙂
https://github.com/hammad-tariq/
https://github.com/hammad-tariq/
Hello Tariq,
Thank you very much for providing source code.
Would you share any other application you built using Kotlin?
At first, thanks for your great tutorial. I guess it was quite some work. But when I tried to follow it I found the code was a bit incomplete, some parts are missing. Then you said you uploaded the code to your github account. I had a look there, but obviously the code is no longer available. Could you be so kind and upload it again?
Thanks a lot,
Lars
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.
The code is no longer available on GitHub. I am very upset. Could you be so kind and download it again? It is very necessary for me.
Thank you very much.