Glide Tutorial for Android: Getting Started

In this Glide Tutorial, you’ll learn how to use Glide to create a photo app that displays pictures to your users with different filters. By Meng Taing.

Leave a rating/review
Download materials
Save for later
Share

Glide is the most recommended image loader for Android. it’s easy to use and requires minimal configuration. Although you might have used Glide to load image from a URL to ImageView, there are many cool features you might not know.

In this Glide Tutorial, you’ll create Wendergram, a simplified Instagram which displays a three-column grid of photos. It also allows you to apply filters to those photos.

Note: For this intermediate-level tutorial, you need a basic knowledge of Kotlin and Android. If you’re new to Android, check out our Android tutorials. If you’re unfamiliar with Kotlin, take a look at Kotlin For Android: An Introduction.

Getting Started

Use the Download Materials button at the top or bottom of this tutorial to download the starter project. Open the starter project in Android Studio 3.3 or above. Build and run the app. The UI skeleton is in place, so you can start with Glide right away.

Adding Permission and Dependencies

If you’re using Glide to load images from URLs, make sure AndroidManifest.xml has INTERNET permission. This is a common mistake that even experienced developers could spend hours debugging.

Open AndroidManifest.xml and add the following permission above the <application> tag:

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

Next, open build.gradle under app directory. Add the following dependencies inside dependencies curly brackets under // TUTORIAL DEPENDENCIES HERE comment:

implementation 'com.github.bumptech.glide:glide:4.9.0'

Finally, press Sync Now and wait until Android Studio finishes syncing your dependencies. Now you’re ready to work with photos using Glide.

Photo Source

The photo URLs used in this tutorial are from https://source.unsplash.com/random. This URL redirects to a random image URL which accepts a ?w= query string for image width that allows you to retrieve the image with any dimensions. This is useful when you have a screen to show thumbnails and another screen to show full resolution images.

Open PhotoProvider.kt. This class returns a list of photo URLs. You’ll notice the first item in the list is null and the second one is https://non-existing-url. Yes, it’s a non-existing URL. The rest of the URLs are valid and will return images. You’ll see how Glide handles these URLs in the next section.

Loading a Photo Gallery

Open PhotoAdapter.kt. This is a RecylerView Adapter. Use onBindViewHolder to bind the data to PhotoViewHolder. Open the declaration of holder.bind(photoUrl) by holding Command (Ctrl key on Windows) and clicking on the word bind. Android Studio opens PhotoViewHolder.kt where you won’t find anything implemented yet in bind(photoUrl: String?). You’ll load all the photos into the gallery here. Add the following code into bind(photoUrl: String?):

val url = if (photoUrl != null) "$photoUrl?w=360" else null //1
Glide.with(itemView)  //2
    .load(url) //3
    .centerCrop() //4
    .placeholder(R.drawable.ic_image_place_holder) //5
    .error(R.drawable.ic_broken_image) //6
    .fallback(R.drawable.ic_no_image) //7
    .into(itemView.ivPhoto) //8
  1. Append ?w=360 to the URL if the URL is not null. This value assumes that the device screen has 1080px in width. You can set this value dynamically to be one-third of the device’s screen width.
  2. Provide a view, fragment or activity to Glide as the life-cycle context.
  3. Load the URL.
  4. Apply the centerCrop transformation so that the photo fully fills the ImageView.
  5. Provide an image resource as a placeholder before Glide starts loading the image.
  6. Provide an image resource as an error placeholder when Glide is unable to load the image. This will be shown for the non-existing-url.
  7. Use fallback image resource when the url can be null.
  8. Set the ImageView of PhotoViewHolder as the target.

Use alt+enter to add import statements if necessary. Run the app to see if all the photos load successfully:

As you can see, the first URL is null, so Glide immediately loads R.drawable.ic_no_image. The second URL is an invalid URL, so Glide displays R.drawable.ic_broken_image after trying to load it. The rest of photos are fine.

Loading a Profile Picture

Open MainActivity.kt and load this URL https://source.unsplash.com/random into the profile picture ImageView. Inside the empty loadProfilePic(profilePicUrl: String), add the following code:

Glide.with(this) //1
    .load(profilePicUrl)
    .placeholder(R.drawable.ic_profile_placeholder)
    .error(R.drawable.ic_profile_placeholder)
    .skipMemoryCache(true) //2
    .diskCacheStrategy(DiskCacheStrategy.NONE) //3
    .transform(CircleCrop()) //4
    .into(ivProfile)
  1. Now that you’re in MainActivity, you can provide this as Glide context.
  2. By default, Glide caches images with URLs as keys in the memory and disk. https://source.unsplash.com/random redirects to a different URL every time. Glide will cache the image it loads for the first time with https://source.unsplash.com/random as the key. If you do not supply this option, Glide will always use the same image from both memory and disk.
  3. Tell Glide not to write any images to the cache.
  4. What would you do if you wanted to have a circular Image view? Normally, you would google to find an existing library. With Glide, you can achieve this through applying transform(CircleCrop()) option.

Run the app to see the circular profile photo:

Picasso Tutorial app

Each time you relaunch the app, you should see a different profile photo, thanks to Unsplash.

Beside CircleCrop(), Glide has FitCenter() built in. You’ll learn more about other Transformations in the next section.

Editing Photos with More Transformations

Reopen or switch build.gradle under the app directory. Add the following dependencies below the Glide dependencies you added earlier:

implementation 'jp.wasabeef:glide-transformations:3.3.0'
implementation 'jp.co.cyberagent.android.gpuimage:gpuimage-library:1.4.1'

The second library is for GPU filters. You can find out more about the library here.

After you sync the project with Gradle files, open EditPhotoActivity.kt. This is where you’ll apply three transformations — Blur, Vignette, and Contrast — to the photo. Each RadioButton and the SeekBar adjusts the value of blurValue, vignetteValue and contrastValue, respectively. Scroll down to applyFilters(progress: Int, photoUrl: String?), and add the following code before the closing curly bracket:

Glide.with(this)
    .load(photoUrl)
    .override(500, 500) //1
    .transform( //2
        CenterCrop(),
        BlurTransformation(blurValue), //3
        ContrastFilterTransformation(contrastValue), //4
        VignetteFilterTransformation(
            PointF(0.5f, 0.5f),
            floatArrayOf(0f, 0f, 0f),
            0f,
            vignetteValue)) //5
    .diskCacheStrategy(DiskCacheStrategy.DATA) //6
    .placeholder(ivPhoto.drawable) //7
    .into(ivPhoto)
  1. Glide automatically limits the size of the image it holds in cache and memory to the ImageView dimensions. The more transformations you apply, the longer Glide takes to load the image. In this case, use override(width, height) to tell Glide to load smaller image.
  2. transform(...) takes one or multiple transformations.
  3. BlurTransformation accepts a blur radius from 1 to 25.
  4. ContrastFilterTransformation accepts contrast values from 0.0 to 4.0, with 1.0 as the normal level.
  5. VignetteFilterTransformation requires center-point position, color, starting location and ending location. The end location value is between 0.0 and 4.0.
  6. Use DiskCacheStrategy.DATA so that Glide only caches the full resolution image, not the transformation.
  7. Provide the current drawable as a placeholder while Glide is doing the transformation. Otherwise, the ImageView will be blank for a brief moment every time the SeekBar is changed.

You can check Glide documentation to learn more about other DiskCacheStrategies.

Run the app. Click on any photo to edit it. Play around with RadioButton and drag the SeekBar to change it. Voila! You made your own Instagram.

Clearing the Cache

There are some good reasons to clear the cache. One reason is to debug. You want to make sure Glide isn’t loading images from memory or disk. Another reason is to allow users to clear some disk space. Someday, you might build an app that catches up to hundreds of megabytes of images!

To clear Glide’s cache, add the following code to clearCache() at the end of MainActivity.kt:

Thread(Runnable {
  Glide.get(this).clearDiskCache() //1
}).start()
Glide.get(this).clearMemory() //2
  1. You can only call clearDiskCache() in background. A simple solution is to call this method in a thread.
  2. You can only call clearMemory() in the main thread.

The option menu item invokes clearCache(). To verify whether the cache is cleared, open the App Info of Wendergram. On some devices or emulators, you can long-press the app icon and drag it to App Icon on the top of the screen.

As you can see, the cache size now is 9.48MB. Run the app and click Clear Cache from the option menu. Now, go back to App Info to check whether the cache size is smaller.

Now the cache size is 24KB. When you launch the app again, you’ll see some photo placeholders for a brief moment before all the images appear. What does it mean? This means Glide has to load all the photos from the network again.

Displaying the Loading Progress Bar

Depending on network speed and image size, loading an image can take up to a few seconds. Your users might not be patient enough to wait on a static screen. It’s a better user experience to show the loading progress bar for such a long-running task.

Although adding a progress bar doesn’t sound very hard, unfortunately, Glide doesn’t expose a download progress listener. In order to listen to Glide’s downloading progress, you need to override the Glide module and provide your own OkHttp integration.

Adding Glide Annotation and OkHttp Integration

Add the following dependency to build.gradle file in the app directly:

kapt 'com.github.bumptech.glide:compiler:4.9.0'
implementation('com.github.bumptech.glide:okhttp3-integration:4.9.0') {
  exclude group: 'glide-parent'
}

Also, add the following line to the top of the same build.gradle file, below other plugins:

apply plugin: 'kotlin-kapt'

kapt is an annotation processor for Kotlin. Don’t mistakenly use annotationProcessor because if you do, your new custom Glide module won’t be used, and it won’t throw any errors.

Sync the project with Gradle file. Grab a coffee before going through the final stretch of the tutorial. There will be a lot of code copying and pasting and explanations. :]

Creating ResponseProgressListener

You’ll have to create multiple interfaces and classes in order to make this work, so create a new package under the root package and name it glide.

Under the glide pacakge you created above, create a new interface named ResponseProgressListener:

interface ResponseProgressListener {
  fun update(url: HttpUrl, bytesRead: Long, contentLength: Long)
}

This interface is responsible for notifying whoever is listening to the downloading progress of the URL.

Creating UIonProgressListener

In the same package as above, create a new interface named UIonProgressListener:

interface UIonProgressListener {
  
  val granularityPercentage: Float //1

  fun onProgress(bytesRead: Long, expectedLength: Long) //2
}

As the name suggests, this interface is responsible for updating the UI, which updates the progress of the ProgressBar.

granularityPercentage controls how often the listener needs an update. 0% and 100% will always be dispatched. For example, if you return one, it will dispatch at most 100 times, with each time representing at least one percent of the progress.

Creating DispatchingProgressManager

You need to create class which not only keeps track of the progress of all URLs but also notifies the UI listeners to update progress in the UI thread. Create a new class named DispatchingProgressManager in the same glide package:

class DispatchingProgressManager internal constructor() : ResponseProgressListener {

  companion object {
    private val PROGRESSES = HashMap<String?, Long>() //1
    private val LISTENERS = HashMap<String?, UIonProgressListener>() //2

    internal fun expect(url: String?, listener: UIonProgressListener) { //3
      LISTENERS[url] = listener
    }

    internal fun forget(url: String?) { //4
      LISTENERS.remove(url)
      PROGRESSES.remove(url)
    }
  }

  private val handler: Handler = Handler(Looper.getMainLooper()) //5

  override fun update(url: HttpUrl, bytesRead: Long, contentLength: Long) {
    val key = url.toString()
    val listener = LISTENERS[key] ?: return //6
    if (contentLength <= bytesRead) { //7
      forget(key)
    }
    if (needsDispatch(key, bytesRead, contentLength, 
        listener.granularityPercentage)) { //8
      handler.post { listener.onProgress(bytesRead, contentLength) }
    }
  }

  private fun needsDispatch(key: String, current: Long, total: Long, granularity: Float): Boolean {
    if (granularity == 0f || current == 0L || total == current) {
      return true
    }
    val percent = 100f * current / total
    val currentProgress = (percent / granularity).toLong()
    val lastProgress = PROGRESSES[key]
    return if (lastProgress == null || currentProgress != lastProgress) { //9
      PROGRESSES[key] = currentProgress
      true
    } else {
      false
    }
  }
}
  1. You need a HashMap to store the progress of the URLs you want to display the ProgressBar.
  2. You also need another HashMap to store the UI listeners.
  3. This is the method to add the URL and its UI listener to the HashMap. You call this at the beginning of the download.
  4. When the download of a URL is completed or returns an error, call this to remove the URL from both HashMaps.
  5. You need a UI thread handler to update the UI because the progress is notified in the background thread.
  6. Not all the URLs must have a progress listener. You don't need to show ProgressBar for a photo thumbnail.
  7. Yay! Download completed. Forget this URL.
  8. Remember the granularityPercentage in UIonProgressListener interface? This is where you decide whether to update the UI if downloaded content length is worthy; i.e., not smaller than the granularity.
  9. Here is the simple explanation. You get the currentProgress by dividing the current percent by the granularityPercentage value. Get the lastProgress from the HashMap, and compare currentProgress and lastProgress to see if there's any change; i.e., currentProgress is greater than lastProgress by the multiple of granularityPercentage. If that's the case, notify UI listener.

Listening to Progress in the ResponseBody of OkHttp

If you've used Retrofit with OkHttpClient, you know you can intercept the request and rebuild the response because you want to append an Authorization header with an access toke. In this case, you can wrap the DispatchingProgressManager into your custom ResponseBody.

Create a new class named OkHttpProgressResponseBody in the glide package with the following code:

class OkHttpProgressResponseBody internal constructor(
    private val url: HttpUrl,
    private val responseBody: ResponseBody,
    private val progressListener: ResponseProgressListener) : ResponseBody() { //1

  //2
  private var bufferedSource: BufferedSource? = null

  //3
  override fun contentType(): MediaType {
    return responseBody.contentType()!!
  }

  //4
  override fun contentLength(): Long {
    return responseBody.contentLength()
  }

  //5
  override fun source(): BufferedSource {
    if (bufferedSource == null) {
      bufferedSource = Okio.buffer(source(responseBody.source()))
    }
    return this.bufferedSource!!
  }

  //6
  private fun source(source: Source): Source {
    return object : ForwardingSource(source) {
      var totalBytesRead = 0L

      @Throws(IOException::class)
      override fun read(sink: Buffer, byteCount: Long): Long {
        val bytesRead = super.read(sink, byteCount)
        val fullLength = responseBody.contentLength()
        if (bytesRead.toInt() == -1) { // this source is exhausted
          totalBytesRead = fullLength
        } else {
          totalBytesRead += bytesRead
        }
        progressListener.update(url, totalBytesRead, fullLength)  //7
        return bytesRead
      }
    }
  }
}
  1. Take the original ResponseBody of OkHttp and return a new ResponseBody, which has your listener embedded in.
  2. Create your own BufferedSource to read the downloaded byte length.
  3. Nothing special here. Just return the content type of the original ResponseBody.
  4. Return the content length of the original ResponseBody.
  5. Recreate the BufferedSource of your own with source(source: Source): Source below.
  6. This is the key part of the progress listening section. This method returns a new Source, which keeps track of totalBytesRead and dispatches it to the listener.

Extending a Glide Module

Create a new class named ProgressAppGlideModule under the glide package with the following code:

@GlideModule  //1
class ProgressAppGlideModule : AppGlideModule() {

  override fun registerComponents(context: Context, glide: Glide, registry: Registry) { //2
    super.registerComponents(context, glide, registry)
    val client = OkHttpClient.Builder()
        .addNetworkInterceptor { chain -> //3
          val request = chain.request()
          val response = chain.proceed(request)
          val listener = DispatchingProgressManager()  //4
          response.newBuilder()
              .body(OkHttpProgressResponseBody(request.url(), response.body()!!, listener))  //5
              .build()
        }
        .build()
    glide.registry.replace(GlideUrl::class.java, InputStream::class.java, OkHttpUrlLoader.Factory(client)) //6
  }
}
  1. Remember the dependency you added with kapt in the build.gradle file earlier? This is where you need it.
  2. The only thing you need to do here is to override the registerComponents(...) and replace it with your own OkHttpClient.
  3. Add an Interceptor to your own OkHttpClient.
  4. This is where you create your DispatchingProgressManager instance.
  5. This is where you provide your own OkHttpProgressResponseBody with the DispatchingProgressManager instance.
  6. Glide provides a OkHttpUrlLoader.Factory class for you to conveniently replace Glide's default networking library.

Wiring Everything Together

This is the final class you've to create under the glide package. You need a class to associate the ImageView with its ProgressBar. Create a GlideImageLoader class:

class GlideImageLoader(
    private val mImageView: ImageView?,
    private val mProgressBar: ProgressBar?) { //1

  fun load(url: String?, options: RequestOptions?) { //2
    if (options == null) return

    onConnecting() //3

    DispatchingProgressManager.expect(url, object : UIonProgressListener { //4

      override val granularityPercentage: Float //5
        get() = 1.0f

      override fun onProgress(bytesRead: Long, expectedLength: Long) {
        if (mProgressBar != null) {
          mProgressBar.progress = (100 * bytesRead / expectedLength).toInt() //6
        }
      }
    })

    Glide.with(mImageView!!.context) //7
        .load(url)
        .apply(options) //8
        .listener(object : RequestListener<Drawable> { //9
          override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?,
              isFirstResource: Boolean): Boolean {
            DispatchingProgressManager.forget(url)
            onFinished()
            return false
          }

          override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable>?,
              dataSource: DataSource?, isFirstResource: Boolean): Boolean {

            DispatchingProgressManager.forget(url)
            onFinished()
            return false
          }
        })
        .into(mImageView)
  }


  private fun onConnecting() {
    if (mProgressBar != null) mProgressBar.visibility = View.VISIBLE
  }

  private fun onFinished() {
    if (mProgressBar != null && mImageView != null) {
      mProgressBar.visibility = View.GONE
      mImageView.visibility = View.VISIBLE
    }
  }
}

If you receive an error saying that "There are no type arguments expected for annotation class target," add the com.bumptech.glide.request.target.Target dependency at the top of your class.

Step by step:

  1. Provide ImageView and its ProgressBar to your GlideImageLoader constructor.
  2. Expose a load(url: String?, options: RequestOptions?) because you'll load the URL with Glide internally. You still accept RequestOption for transformations, placeholders, etc.
  3. When starting to load the image, make sure the ProgressBar is visible.
  4. This is where you call the expect(...) to store the progress and reference to the listener you initialize here.
  5. Set granularityPercentage with a value of 1 so that the listener will be called for every minimum one percent of the progress changed.
  6. Update the ProgressBar's progress.
  7. Call Glide to load an image here internally.
  8. Apply the options from the parameters.
  9. Glide exposes a resource listener so that you can perform extra actions if the image resource is loaded with success or failure. In either case, you want your ProgressAppGlideModule to forget the URL, hide the ProgressBar and make sure the ImageView is visible.

Up to this point, your glide package should have all the classes as shown in the screenshot below:

Applying ProgressListener to PhotoViewHolder

If you observed the layout of EditPhotoActivity closely, you might have seen a ProgressBar ready for you to manipulate.

Go back to EditPhotoActivity.kt. Right below the class declaration of EditPhotoActivity, create a private instance of GlideImageLoader as follows:

private var glideImageLoader: GlideImageLoader? = null

You can only initialize glideImageLoader inside onCreate right below setContentView(R.layout.activity_edit_photo):

glideImageLoader = GlideImageLoader(ivPhoto, progressBar)

Replace the code where you loaded the image URL with Glide before the closing bracket of applyFilters(progress: Int, photoUrl: String?) with the code below:

glideImageLoader?.load(
    photoUrl,
    RequestOptions()
        .override(500, 500)
        .transform(
            CenterCrop(),
            BlurTransformation(blurValue),
            ContrastFilterTransformation(contrastValue),
            VignetteFilterTransformation(
                PointF(0.5f, 0.5f),
                floatArrayOf(0f, 0f, 0f),
                0f,
                vignetteValue))
        .diskCacheStrategy(DiskCacheStrategy.DATA)
        .placeholder(ivPhoto.drawable))

This is similar to what you did earlier. Instead of calling Glide.with(...).load(...).into(...) directly, now you use your own glideImageLoader to do the job. You can still pass all the request options with the default Glide loader.

Run the app to check whether the ProgressBar in the EditPhotoActivity is updating as the photo is being loaded. If you don't see a ProgressBar, the photo is probably loaded from the cache. Click the Clear Cache menu item on the MainActivity screen, and then click on the photo you want to edit again:

Where to Go From Here?

You've just built your very own Instagram with Glide! You can extend the functionality of the app by allowing users to share the edited photos on social media or save to the device storage. There are many other cool things you can do with Glide such as loading GIFs and applying crossfade transitions. Keep Gliding!

Download the final project by clicking the Download Materials button at the top or bottom of this tutorial.

If you're curious about Glide's performance compared with other image loading libraries such as Picasso or Fresco, you can build a simple app that shows the downloading duration of each library. By now, you should know how to use Glide’s Request Listener and be ready to give it a try.

If you have any questions or comments, please join the forum discussion.