How to load images without using the third-party library in Android Kotlin.
Today we learned how to load images without using the third-party library in Android Kotlin for that I used Unsplash API here for images.
For API implementation I am using this website.
https://unsplash.com/developers
I used the MVVM pattern with a recycler view.
First of all, we need to add view binding for that I added a build feature In the build. gradle.kts file after that I add dependencies in the gradle file.
build. gradle.kts
buildFeatures {
viewBinding = true
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
//retrofit
implementation ("com.squareup.retrofit2:retrofit:2.9.0")
implementation ("com.squareup.retrofit2:converter-gson:2.9.0")
implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4") //Coroutains
implementation ("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0") //viewModel scope
implementation ("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") //lifecycle scope
implementation ("androidx.activity:activity-ktx:1.8.2")
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
}
We need some permission so I added permission in the Android manifest file.
AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
Now we will create our XML design in the activity_main.xml file.
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.MainActivity">
<Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/image_list"
android:textColor="@color/white" />
</Toolbar>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_images"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="8dp"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar" />
<ProgressBar
android:id="@+id/progressbar"
android:layout_width="48dp"
android:layout_height="48dp"
android:progressTint="@color/colorPrimary"
android:elevation="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar" />
</androidx.constraintlayout.widget.ConstraintLayout>
image_item.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:clipChildren="true"
android:elevation="4dp"
app:cardCornerRadius="8dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/img"
android:layout_width="160dp"
android:layout_height="160dp"
android:background="@android:color/background_dark"
android:contentDescription="@string/imageitem"
android:scaleType="centerCrop"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
Now we need to create a client class for retrofit.
RetrofitClient.kt
package com.example.imageloadingapp.data.api
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
object RetrofitClient {
private var retrofit: Retrofit? = null
val client: Retrofit?
get() {
retrofit = Retrofit.Builder()
.baseUrl(Constant.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
return retrofit
}
}
After adding the retrofit client class we need to add an API interface.
ImageApi.kt
package com.example.imageloadingapp.data.api
import com.example.imageloadingapp.data.model.response.ImageListResponse
import retrofit2.http.GET
import retrofit2.http.Query
interface ImageApi {
@GET("/photos/")
suspend fun getListPhotos(@Query("client_id") clientId: String,@Query("page") page: Int,@Query("per_page") per_page: Int): ImageListResponse
companion object {
fun getApi(): ImageApi? {
return RetrofitClient.client?.create(ImageApi::class.java)
}
}
}
After that, we create one object file to add the base URL.
Constant. kt
package com.example.imageloadingapp.data.api
class Constant {
companion object {
const val BASE_URL="https://api.unsplash.com"
const val CLIENT_ID="uuWFm7wKJ5bxuK2x2LYi1NmpGaSY302wx3K_Bzvt3Dg"
}
}
BaseResponse.kt
package com.example.imageloadingapp.data.api
sealed class BaseResponse<out T> {
data class Success<out T>(val data: T? = null) : BaseResponse<T>()
data class Loading(val nothing: Nothing?=null) : BaseResponse<Nothing>()
data class Failed(val msg: String?) : BaseResponse<Nothing>()
}
Now we need to create an API response class.
ImageListResponse.kt
package com.example.imageloadingapp.data.model.response
import com.google.gson.annotations.SerializedName
class ImageListResponse : ArrayList<ImageListResponse.PhotoListResponseItem>() {
data class PhotoListResponseItem(
@SerializedName("alt_description")
val altDescription: String,
@SerializedName("alternative_slugs")
val alternativeSlugs: AlternativeSlugs,
@SerializedName("asset_type")
val assetType: String,
@SerializedName("blur_hash")
val blurHash: String,
@SerializedName("breadcrumbs")
val breadcrumbs: List<Breadcrumb>,
@SerializedName("color")
val color: String,
@SerializedName("created_at")
val createdAt: String,
@SerializedName("current_user_collections")
val currentUserCollections: List<Any>,
@SerializedName("description")
val description: String,
@SerializedName("height")
val height: Int,
@SerializedName("id")
val id: String,
@SerializedName("liked_by_user")
val likedByUser: Boolean,
@SerializedName("likes")
val likes: Int,
@SerializedName("links")
val links: Links,
@SerializedName("promoted_at")
val promotedAt: String,
@SerializedName("slug")
val slug: String,
@SerializedName("sponsorship")
val sponsorship: Any,
@SerializedName("topic_submissions")
val topicSubmissions: TopicSubmissions,
@SerializedName("updated_at")
val updatedAt: String,
@SerializedName("urls")
val urls: Urls,
@SerializedName("user")
val user: User,
@SerializedName("width")
val width: Int
) {
data class AlternativeSlugs(
@SerializedName("de")
val de: String,
@SerializedName("en")
val en: String,
@SerializedName("es")
val es: String,
@SerializedName("fr")
val fr: String,
@SerializedName("it")
val `it`: String,
@SerializedName("ja")
val ja: String,
@SerializedName("ko")
val ko: String,
@SerializedName("pt")
val pt: String
)
data class Breadcrumb(
@SerializedName("index")
val index: Int,
@SerializedName("slug")
val slug: String,
@SerializedName("title")
val title: String,
@SerializedName("type")
val type: String
)
data class Links(
@SerializedName("download")
val download: String,
@SerializedName("download_location")
val downloadLocation: String,
@SerializedName("html")
val html: String,
@SerializedName("self")
val self: String
)
data class TopicSubmissions(
@SerializedName("earth-hour")
val earthHour: EarthHour,
@SerializedName("people")
val people: People
) {
data class EarthHour(
@SerializedName("approved_on")
val approvedOn: String,
@SerializedName("status")
val status: String
)
data class People(
@SerializedName("approved_on")
val approvedOn: String,
@SerializedName("status")
val status: String
)
}
data class Urls(
@SerializedName("full")
val full: String,
@SerializedName("raw")
val raw: String,
@SerializedName("regular")
val regular: String,
@SerializedName("small")
val small: String,
@SerializedName("small_s3")
val smallS3: String,
@SerializedName("thumb")
val thumb: String
)
data class User(
@SerializedName("accepted_tos")
val acceptedTos: Boolean,
@SerializedName("bio")
val bio: String,
@SerializedName("first_name")
val firstName: String,
@SerializedName("for_hire")
val forHire: Boolean,
@SerializedName("id")
val id: String,
@SerializedName("instagram_username")
val instagramUsername: String,
@SerializedName("last_name")
val lastName: String,
@SerializedName("links")
val links: Links,
@SerializedName("location")
val location: String,
@SerializedName("name")
val name: String,
@SerializedName("portfolio_url")
val portfolioUrl: String,
@SerializedName("profile_image")
val profileImage: ProfileImage,
@SerializedName("social")
val social: Social,
@SerializedName("total_collections")
val totalCollections: Int,
@SerializedName("total_illustrations")
val totalIllustrations: Int,
@SerializedName("total_likes")
val totalLikes: Int,
@SerializedName("total_photos")
val totalPhotos: Int,
@SerializedName("total_promoted_illustrations")
val totalPromotedIllustrations: Int,
@SerializedName("total_promoted_photos")
val totalPromotedPhotos: Int,
@SerializedName("twitter_username")
val twitterUsername: String,
@SerializedName("updated_at")
val updatedAt: String,
@SerializedName("username")
val username: String
) {
data class Links(
@SerializedName("followers")
val followers: String,
@SerializedName("following")
val following: String,
@SerializedName("html")
val html: String,
@SerializedName("likes")
val likes: String,
@SerializedName("photos")
val photos: String,
@SerializedName("portfolio")
val portfolio: String,
@SerializedName("self")
val self: String
)
data class ProfileImage(
@SerializedName("large")
val large: String,
@SerializedName("medium")
val medium: String,
@SerializedName("small")
val small: String
)
data class Social(
@SerializedName("instagram_username")
val instagramUsername: String,
@SerializedName("paypal_email")
val paypalEmail: Any,
@SerializedName("portfolio_url")
val portfolioUrl: String,
@SerializedName("twitter_username")
val twitterUsername: String
)
}
}
}
ImageRepository.kt
package com.example.imageloadingapp.data.repository
import com.example.imageloadingapp.data.api.Constant
import com.example.imageloadingapp.data.api.ImageApi
import com.example.imageloadingapp.data.model.response.ImageListResponse
class ImageRepository(val imageApi: ImageApi) {
suspend fun getUserData(page:Int,perPage:Int): ImageListResponse {
return imageApi.getListPhotos(Constant.CLIENT_ID,page,perPage)
}
}
Now we will create the view model file.
MainViewModel.kt
package com.example.imageloadingapp.viewmodel
import com.example.imageloadingapp.data.api.ImageApi
import com.example.imageloadingapp.data.model.response.ImageListResponse
import com.example.imageloadingapp.data.repository.ImageRepository
import android.util.Log
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.example.imageloadingapp.data.api.BaseResponse
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.lang.Exception
class MainViewModel :ViewModel(){
val api = ImageApi.getApi()
val repository = api?.let { ImageRepository(it) }
val listData = MutableLiveData<BaseResponse<ImageListResponse>>()
fun getImageList(page: Int, perPage: Int) {
listData.value=BaseResponse.Loading()
CoroutineScope(Dispatchers.IO).launch {
try {
val response = repository?.getUserData(page, perPage)
listData.postValue(BaseResponse.Success(response))
} catch (ex: Exception) {
listData.postValue(BaseResponse.Failed(ex.message))
Log.e("Error", ex.message.toString())
}
}
}
}
We need one image loader class for loading the image for that here I create ImageLoader.kt
ImageLoader.kt
package com.example.imageloadingapp.utils.imageloadinghelper
import android.R
import android.app.Activity
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.widget.ImageView
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.InputStream
import java.io.OutputStream
import java.lang.ref.SoftReference
import java.net.HttpURLConnection
import java.net.URL
import java.util.Collections
import java.util.WeakHashMap
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
object LoadImage {
fun loadImage(context: Context, url: String, imageView:ImageView) {
val imageLoader = ImageLoader(context)
imageLoader.displayImage(url,R.drawable.dark_header,imageView)
}
}
class ImageLoader(context: Context?) {
var memoryCache: MemoryCache = MemoryCache()
var fileCache: FileCache
private val imageViews = Collections.synchronizedMap(WeakHashMap<ImageView, String>())
var executorService: ExecutorService
var stub_id: Int = R.drawable.dark_header
init {
fileCache = context?.let { FileCache(it) }!!
executorService = Executors.newFixedThreadPool(5)
}
fun displayImage(url: String, loader: Int, imageView: ImageView) {
stub_id = loader
imageViews[imageView] = url
val bitmap: Bitmap? = memoryCache.get(url)
if (bitmap != null) imageView.setImageBitmap(bitmap) else {
queuePhoto(url, imageView)
imageView.setImageResource(loader)
}
}
private fun queuePhoto(url: String, imageView: ImageView) {
val p = PhotoToLoad(url, imageView)
executorService.submit(PhotosLoader(p))
}
private fun getBitmap(url: String): Bitmap? {
val f: File = fileCache.getFile(url)
//from SD cache
val b = decodeFile(f)
return b
?: try {
var bitmap: Bitmap? = null
val imageUrl = URL(url)
val conn = imageUrl.openConnection() as HttpURLConnection
conn.setConnectTimeout(30000)
conn.setReadTimeout(30000)
conn.instanceFollowRedirects = true
val `is` = conn.inputStream
val os: OutputStream = FileOutputStream(f)
Utils.CopyStream(`is`, os)
os.close()
bitmap = decodeFile(f)
bitmap
} catch (ex: Exception) {
ex.printStackTrace()
null
}
//from web
}
//decodes image and scales it to reduce memory consumption
private fun decodeFile(f: File): Bitmap? {
try {
//decode image size
val o = BitmapFactory.Options()
o.inJustDecodeBounds = true
BitmapFactory.decodeStream(FileInputStream(f), null, o)
//Find the correct scale value. It should be the power of 2.
val REQUIRED_SIZE = 70
var width_tmp = o.outWidth
var height_tmp = o.outHeight
var scale = 1
while (true) {
if (width_tmp / 2 < REQUIRED_SIZE || height_tmp / 2 < REQUIRED_SIZE) break
width_tmp /= 2
height_tmp /= 2
scale *= 2
}
//decode with inSampleSize
val o2 = BitmapFactory.Options()
o2.inSampleSize = scale
return BitmapFactory.decodeStream(FileInputStream(f), null, o2)
} catch (e: FileNotFoundException) {
}
return null
}
//Task for the queue
inner class PhotoToLoad(var url: String, var imageView: ImageView)
internal inner class PhotosLoader(var photoToLoad: PhotoToLoad) : Runnable {
override fun run() {
if (imageViewReused(photoToLoad)) return
val bmp = getBitmap(photoToLoad.url)
bmp?.let { memoryCache.put(photoToLoad.url, it) }
if (imageViewReused(photoToLoad)) return
val bd = BitmapDisplayer(bmp, photoToLoad)
val a = photoToLoad.imageView.context as Activity
a.runOnUiThread(bd)
}
}
fun imageViewReused(photoToLoad: PhotoToLoad): Boolean {
val tag = imageViews[photoToLoad.imageView]
return if (tag == null || tag != photoToLoad.url) true else false
}
//Used to display bitmap in the UI thread
internal inner class BitmapDisplayer(var bitmap: Bitmap?, var photoToLoad: PhotoToLoad) :
Runnable {
override fun run() {
if (imageViewReused(photoToLoad)) return
if (bitmap != null) photoToLoad.imageView.setImageBitmap(bitmap) else photoToLoad.imageView.setImageResource(
stub_id
)
}
}
fun clearCache() {
memoryCache.clear()
fileCache.clear()
}
}
class FileCache(context: Context) {
private var cacheDir: File? = null
init {
//Find the dir to save cached images
cacheDir = context.cacheDir
if (!cacheDir!!.exists()) cacheDir!!.mkdirs()
}
fun getFile(url: String): File {
val filename = url.hashCode().toString()
return File(cacheDir, filename)
}
fun clear() {
val files = cacheDir!!.listFiles() ?: return
for (f in files) f.delete()
}
}
class MemoryCache {
private val cache = Collections.synchronizedMap(HashMap<String, SoftReference<Bitmap>>())
operator fun get(id: String): Bitmap? {
if (!cache.containsKey(id)) return null
val ref = cache[id]!!
return ref.get()
}
fun put(id: String, bitmap: Bitmap) {
cache[id] = SoftReference(bitmap)
}
fun clear() {
cache.clear()
}
}
object Utils {
fun CopyStream(`is`: InputStream, os: OutputStream) {
val buffer_size = 1024
try {
val bytes = ByteArray(buffer_size)
while (true) {
val count = `is`.read(bytes, 0, buffer_size)
if (count == -1) break
os.write(bytes, 0, count)
}
} catch (ex: java.lang.Exception) {
}
}
}
Here we code for the ImageLoaderAdapter.kt file.
ImageLoaderAdapter.kt
package com.example.imageloadingapp.ui.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.example.imageloadingapp.data.model.response.ImageListResponse
import com.example.imageloadingapp.databinding.ImageItemBinding
import com.example.imageloadingapp.utils.imageloadinghelper.LoadImage
class ImageLoaderAdapter : RecyclerView.Adapter<ImageLoaderAdapter.ImageViewHolder>() {
private val listImages = mutableListOf<ImageListResponse.PhotoListResponseItem>()
open class ImageViewHolder(val binding: ImageItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bindData(item: ImageListResponse.PhotoListResponseItem) {
LoadImage.loadImage(binding.img.context, item.urls.regular, binding.img)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageViewHolder {
val binding = ImageItemBinding
.inflate(LayoutInflater.from(parent.context), parent, false)
return ImageViewHolder(binding)
}
override fun getItemCount(): Int {
return listImages.size
}
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
holder.bindData(listImages.get(position))
}
fun updateData(listData:List<ImageListResponse.PhotoListResponseItem>){
listImages.addAll(listData)
notifyDataSetChanged()
}
}
Now we code for the MainActivity.kt file.
MainActivity.kt
package com.example.imageloadingapp.ui
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.example.imageloadingapp.databinding.ActivityMainBinding
import com.example.imageloadingapp.ui.adapter.ImageLoaderAdapter
import android.view.View
import android.widget.Toast
import androidx.activity.viewModels
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.imageloadingapp.data.api.BaseResponse
import com.example.imageloadingapp.viewmodel.MainViewModel
class MainActivity : AppCompatActivity() {
val adapter = ImageLoaderAdapter()
private val viewModel by viewModels<MainViewModel>()
lateinit var binding: ActivityMainBinding
var isLoading: Boolean = false
var isLastPage: Boolean = false
var page = 0
var pageSize = 30
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.rvImages.adapter = adapter
binding.rvImages.layoutManager = GridLayoutManager(
this, 2
)
observeData()
getImageList()
binding.rvImages.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val manager = (binding.rvImages.layoutManager as GridLayoutManager)
val visibleCount = manager.childCount
val totalItem = manager.itemCount
val firstVisibleItem = manager.findFirstVisibleItemPosition()
if (!isLoading && !isLastPage) {
if ((visibleCount + firstVisibleItem >= totalItem) &&
firstVisibleItem >= 0 && totalItem >= pageSize
) {
page++
getImageList()
}
}
}
})
}
private fun getImageList() {
viewModel.getImageList(page, pageSize)
}
fun setProgressBarVisible(isVisible: Boolean) {
isLoading=isVisible
binding.progressbar.visibility = if (isVisible) View.VISIBLE else View.GONE
}
private fun observeData() {
viewModel.listData.observe(this) {
runOnUiThread {
when (it) {
is BaseResponse.Loading -> {
setProgressBarVisible(true)
}
is BaseResponse.Success -> {
setProgressBarVisible(false)
isLoading = false
if (it.data?.size!! > 0) {
isLastPage = it.data.size < pageSize
} else {
isLastPage = true
}
it.data?.let { it1 -> adapter.updateData(it1) }
}
is BaseResponse.Failed -> {
setProgressBarVisible(false)
Toast.makeText(this@MainActivity, "Api Failed" + it.msg, Toast.LENGTH_LONG)
.show()
}
}
}
}
}
}
Thats it. Happy Coding :)