Although Glide provides out of the box support for most common types of models (URLs, Uris, file paths etc), you may occasionally run into a type that Glide doesn’t support. You may also run in to cases where you want to customize or tweak Glide’s default behavior. You may even want to integrate a new way of fetching images or a new networking library beyond those available in Glide’s integration libraries.

Fortunately Glide is extensible. To add support for a new type of model, you’ll need to follow three steps:

  1. Implement a ModelLoader
  2. Implement a DataFetcher that can be returned by your ModelLoader
  3. Register your new ModelLoader with Glide using an AppGlideModule (or LibraryGlideModule if you’re working on a library rather than an application).

So that we have something to follow along with, let’s implement a custom ModelLoader that takes Base64 encoded image Strings and decodes them with Glide. Note that if you actually want to do this in your application, it would be better to retrieve the Base64 encoded Strings in your ModelLoader so that you can avoid the CPU and memory overhead of loading them into memory if Glide has previously cached your image.

For our purposes though, loading a Base64 image should provide a simple example, even if it might be a bit inefficient in the real world.

Writing the ModelLoader.

The first step is to implement the ModelLoader interface. Before we do so, we need to make two decisions:

  1. What type of Model should we handle?
  2. What type of Data should we produce for that Model?

In this case we’d like to handle base 64 encoded Strings, so that means String is probably a reasonable choice for our Model type. Later on we’ll need something more specific than just any random String, but for now, String is sufficient to start our implementation.

Next we need to decide what type of data we should try to obtain from our String. By default, Glide provides image decoders for two types of data:

  1. InputStream
  2. ByteBuffer

Glide also provides default support for ParcelFileDescriptor for decoding videos.

Since we’re decoding an Image, we probably want InputStream or ByteBuffer. Since we already have all of the data in memory and the methods in Base64, which we’ll be using to do the actual decoding, return byte[], ByteBuffer is probably the best choice for our data.

An empty implementation

Now that we know our Model and Data types, we can create a class that accepts the right types and returns default values:

package judds.github.com.base64modelloaderexample;

import android.support.annotation.Nullable;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.model.ModelLoader;
import java.io.InputStream;
import java.nio.ByteBuffer;

/**
 * Loads an {@link InputStream} from a Base 64 encoded String.
 */
public final class Base64ModelLoader implements ModelLoader<String, ByteBuffer> {

  @Nullable
  @Override
  public LoadData<ByteBuffer> buildLoadData(String model, int width, int height, Options options) {
    return null;
  }

  @Override
  public boolean handles(String model) {
    return false;
  }
}

Of course this ModelLoader won’t do much, but it’s a start.

Implementing handles()

The next step is to implement the handles() method. As we mentioned earlier, there are a number of different types of Models that a String might be, including:

  1. URLs
  2. Uris
  3. File paths

The handles() method allows the ModelLoader to efficiently check each model and avoid loading unsupported types.

To make our jobs easier, let’s assume that our base64 encoded Strings will actually be passed to us as data URIs. The goal of the handles method is to identify any Strings passed in to Glide that match the data uri format and return true for only those strings.

Fortunately the data URI format seems pretty straight forward, so we can just check for the data: prefix:

  @Override
  public boolean handles(String model) {
    return model.startsWith("data:");
  }

Depending on how robust we want our implementation to be, we might also want to check for the embedded image type or the format of the data, so that we don’t try to load the bytes for an html page for example. We’ll skip that here for now for simplicities sake.

Implementing buildLoadData

Now that we’re able to identify data URIs, the next step is to provide an object that can decode the actual ByteBuffer if it’s not already in cache for our given model, dimensions, and options. To do so we need to implement the buildLoadData method.

To start, our method just returns null, which is perfectly valid, although not very useful:

  @Nullable
  @Override
  public LoadData<ByteBuffer> buildLoadData(String model, int width, int height, Options options) {
    return null;
  }

To make our method more useful, let’s start by returning a new LoadData<ByteBuffer> object. To do so, we’re going to need two things:

  1. A Key that will be used as part of our disk cache keys (the model’s equals() and hashCode() methods are used for the in memory cache key).
  2. A DataFetcher that can obtain a ByteBuffer for our particular model.

Picking the Key

The Key for the disk cache is straight forward in this case because our model type is a String. If you have a model type that can be serialized using toString(), you can just pass your model into a new ObjectKey:

Key diskCacheKey = new ObjectKey(model);

Otherwise, you’d want to implement the Key interface here too, making sure that the equals(), hashCode() and updateDiskCacheKey methods were all filled out and uniquely and consistently identified your particular model.

Since we’re literally working with Strings here, ObjectKey will work just fine.

Picking the DataFetcher

Since we’re adding support for a new Model, we’re actually going to want a custom DataFetcher. In some cases ModelLoaders may actually just do a bit of parsing in buildLoadData and delegate to another ModelLoader, but we’re not so lucky here.

For now, let’s just pass in null here, even though it’s not a valid value, and move on to our actual DataFetcher implementation:

  @Nullable
  @Override
  public LoadData<ByteBuffer> buildLoadData(String model, int width, int height, Options options) {
    return new LoadData<>(new ObjectKey(model), /*fetcher=*/ null);
  }

Writing the DataFetcher

Like ModelLoader, the DataFetcher interface is generic and requires us to specify the type of data we expect it to return. Fortunately we already decided that we wanted to support loading ByteBuffers, so there’s no difficult decision to make.

As a result, we can quickly stub out an implementation:

public class Base64DataFetcher implements DataFetcher<ByteBuffer> {

  @Override
  public void loadData(Priority priority, DataCallback<? super ByteBuffer> callback) {}

  @Override
  public void cleanup() {}

  @Override
  public void cancel() {}

  @NonNull
  @Override
  public Class<ByteBuffer> getDataClass() {
    return null;
  }

  @NonNull
  @Override
  public DataSource getDataSource() {
    return null;
  }
}

Although there are a number of methods here, most of them are actually pretty easy to implement.

getDataClass

getDataClass() is trivial, we’re loading ByteBuffer:

  @NonNull
  @Override
  public Class<ByteBuffer> getDataClass() {
    return ByteBuffer.class;
  }

getDataSource

getDataSource() is almost as trivial, but it has some implications. Glide’s default caching strategy is different for local images than it is for remote images. Glide assumes that it’s easy and cheap to retrieve local images, so we default to caching images after they’ve been downsampled and transformed. In contrast, Glide assumes that it’s difficult and expensive to retrieve remote images, so we default to caching the original data we retrieved.

For base64 Strings, the best choice for your app might depend on how you retrieve the Strings. If they’re loaded from a local database, DataSource.LOCAL makes the most sense. If you’re retrieving them via HTTP every time, DataSource.REMOTE is a better choice.

Let’s assume the Strings are obtained locally for now:

  @NonNull
  @Override
  public DataSource getDataSource() {
    return DataSource.LOCAL;
  }

cancel

For networking libraries or long running loads where cancellation is possible, it’s a good idea to implement the cancel() method. Doing so will help speed up other queued loads and will save some amount of CPU, memory, or other resources.

In our case, Base64 doesn’t offer a cancellation API, so we can leave it blank:

  @Override
  public void cancel() {
    // Intentionally empty.
  }

cleanup

cleanup() is an interesting one. If you’re loading an InputStream or opening any kind of I/O resources, you absolutely must close and clean up the InputStream or resource in the cleanup() method.

However, in our case we’re just decoding an in memory model into in memory data. As a result, there’s nothing to clean up, so our method can also be empty:

  @Override
  public void cleanup() {
    // Intentionally empty only because we're not opening an InputStream or another I/O resource!
  }

Danger! Make sure that if you open an I/O resource or an InputStream you must close it here! We can only get away with leaving this method blank because we’re not doing so! :)

loadData

Now for the fun part! loadData() is the method where Glide expects you to do your heavy lifting. You can queue an asynchronous task, start a network request, load some data from disk or whatever you’d like. loadData() is always called on one of Glide’s background threads. A given DataFetcher will only be used on a single background thread at a time, so it doesn’t need to be thread safe. However, multiple DataFetchers may be run in parallel, so any shared resources accessed by DataFetchers should be thread safe.

loadData() provides two arguments:

  1. Priority, which can be used to prioritized requests if you’re using a networking library or other queueing system.
  2. DataCallback which should be called with either your decoded data, or an error message if your load fails for any reason.

We can either queue an async task and call the given DataCallback asynchronously, or we can do some work inside the loadData() method and call the DataCallback directly.

In our case we don’t have any network or other queue to call, so we can just do our work in line.

Note that one important thing is missing here. We don’t have a reference to our model! This is because each DataFetcher is basically a closure that can be used to obtain data for a particular model. As a result, Glide expects you to pass the model into the constructor of the DataFetcher:

  private final String model;

  Base64DataFetcher(String model) {
    this.model = model;
  }

As it turns out, our loadData() method is now actually rather simple. We just need to parse out the base64 section of the data uri:

  private String getBase64SectionOfModel() {
    // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs.
    int startOfBase64Section = model.indexOf(',');
    return model.substring(startOfBase64Section + 1);
  }

Then we need to decode the byte[]s of the base64 section:

  byte[] data = Base64.decode(base64Section, Base64.DEFAULT);

And convert it to a ByteBuffer:

  ByteBuffer byteBuffer = ByteBuffer.wrap(data);

Then we just need to call the callback with our decoded ByteBuffer:

  callback.onDataReady(byteBuffer);

With everything together, here’s the complete loadData() implementation:

  @Override
  public void loadData(Priority priority, DataCallback<? super ByteBuffer> callback) {
    String base64Section = getBase64SectionOfModel();
    byte[] data = Base64.decode(base64Section, Base64.DEFAULT);
    ByteBuffer byteBuffer = ByteBuffer.wrap(data);
    callback.onDataReady(byteBuffer);
  }

  private String getBase64SectionOfModel() {
    // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs.
    int startOfBase64Section = model.indexOf(',');
    return model.substring(startOfBase64Section + 1);
  }

The full DataFetcher

Now that we’ve got all the methods in DataFetcher implemented, let’s take one more look at it all together:

package judds.github.com.base64modelloaderexample;

import android.support.annotation.NonNull;
import android.util.Base64;
import com.bumptech.glide.Priority;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.data.DataFetcher;
import java.nio.ByteBuffer;

public class Base64DataFetcher implements DataFetcher<ByteBuffer> {

  private final String model;

  Base64DataFetcher(String model) {
    this.model = model;
  }

  @Override
  public void loadData(Priority priority, DataCallback<? super ByteBuffer> callback) {
    String base64Section = getBase64SectionOfModel();
    byte[] data = Base64.decode(base64Section, Base64.DEFAULT);
    ByteBuffer byteBuffer = ByteBuffer.wrap(data);
    callback.onDataReady(byteBuffer);
  }

  private String getBase64SectionOfModel() {
    // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs.
    int startOfBase64Section = model.indexOf(',');
    return model.substring(startOfBase64Section + 1);
  }

  @Override
  public void cleanup() {
    // Intentionally empty only because we're not opening an InputStream or another I/O resource!
  }

  @Override
  public void cancel() {
    // Intentionally empty.
  }

  @NonNull
  @Override
  public Class<ByteBuffer> getDataClass() {
    return ByteBuffer.class;
  }

  @NonNull
  @Override
  public DataSource getDataSource() {
    return DataSource.LOCAL;
  }
}

Finishing off the ModelLoader.

Back when we were working on our ModelLoader, we left the buildLoadData method a little incomplete and returned null instead of a valid DataFetcher:

  @Nullable
  @Override
  public LoadData<ByteBuffer> buildLoadData(String model, int width, int height, Options options) {
    return new LoadData<>(new ObjectKey(model), /*fetcher=*/ null);
  }

Now that we have a DataFetcher implementation, we can fill that part in:

  @Override
  public LoadData<ByteBuffer> buildLoadData(String model, int width, int height, Options options) {
    return new LoadData<>(new ObjectKey(model), new Base64DataFetcher(model));
  }

We can drop @Nullable since we’re never actually going to return null in our implementation. If we were delegating to a wrapped ModelLoader, we’d want to check that ModelLoaders return value and be sure to return null if it returned null. In some cases we may actually discover while attempting to parse our data that we can’t actually load it, in which case we can also return null.

Registering our ModelLoader with Glide.

We’re almost done, but there’s one last step. Our ModelLoader implementation is complete, but totally unused. To finish off our project, we need to tell Glide about our ModelLoader so that Glide knows to use it.

Adding the AppGlideModule

To do so, we’re going to follow the steps on the configuration page for our application and add an AppGlideModule if you haven’t already done so:

package judds.github.com.base64modelloaderexample;

import com.bumptech.glide.annotation.GlideModule;
import com.bumptech.glide.module.AppGlideModule;

@GlideModule
public class MyAppGlideModule extends AppGlideModule { }

Don’t forget to add a dependency on Glide’s annotation processor to your build.gradle file as well:

annotationProcessor 'com.github.bumptech.glide:compiler:4.8.0'

Next we want to get at Glide’s Registry, so we’ll implement the registerComponents method in our AppGlideModule:

package judds.github.com.base64modelloaderexample;

import com.bumptech.glide.annotation.GlideModule;
import com.bumptech.glide.module.AppGlideModule;

@GlideModule
public class MyAppGlideModule extends AppGlideModule { 
  @Override
  public void registerComponents(Context context, Glide glide, Registry registry) {
    // TODO: implement this.
  }
}

Picking our Registry method

To tell Glide about our ModelLoader, we need to add it to the Registry using one of the available methods for ModelLoaders.

ModelLoaders are stored in a list in the order they are registered. When you start a new load, Glide looks at all the registered ModelLoaders for the model type you provide in the order they were registered and attempts them in order.

As a result, if you’re adding support for a new Model type you typically want to prepend() your ModelLoader so that Glide attempts it before the default ModelLoaders. In our case, we’re doing exactly that, adding support for a new type of model, so we want prepend().

However, that there’s one more wrinkle here. prepend() takes a ModelLoaderFactory, not a ModelLoader. Doing so allows you to delegate to other ModelLoaders, even when they’re registered dynamically, but it also adds an interface you have to implement when defining new loaders.

Implementing ModelLoaderFactory

Fortunately the ModelLoaderFactory interface is quite simple, so we can add it easily:

package judds.github.com.base64modelloaderexample;

import com.bumptech.glide.load.model.ModelLoader;
import com.bumptech.glide.load.model.ModelLoaderFactory;
import com.bumptech.glide.load.model.MultiModelLoaderFactory;
import java.nio.ByteBuffer;

public class Base64ModelLoaderFactory implements ModelLoaderFactory<String, ByteBuffer> {

  @Override
  public ModelLoader<String, ByteBuffer> build(MultiModelLoaderFactory unused) {
    return new Base64ModelLoader();
  }

  @Override
  public void teardown() { 
    // Do nothing.
  }
}

The types for ModelLoaderFactory match those used in our ModelLoader exactly.

Registering our ModelLoader

Finally we just need to update our AppGlideModule to use our new Factory:

@GlideModule
public class MyAppGlideModule extends AppGlideModule {
  @Override
  public void registerComponents(Context context, Glide glide, Registry registry) {
    registry.prepend(String.class, ByteBuffer.class, new Base64ModelLoaderFactory());
  }
}

And that’s it!

Now we can just take any data uri with a base64 encoded image and load it with Glide and it just works:

String dataUri = "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYA..."
Glide.with(fragment)
  .load(dataUri)
  .into(imageView);

Complete sample.

A complete sample project using the code we’ve written is available here: https://github.com/sjudd/Base64ModelLoaderExample.

The commits are in the same order we wrote the code in the sample:

  1. An empty project with a blank Activity
  2. A ModelLoader that just implements handles
  3. A data fetcher implementation
  4. A ModelLoader with a complete buildLoadData implementation
  5. An AppGlideModule and registered ModelLoader
  6. An example data Uri loaded into a View

Caveats

As it turns out, Glide already supports Data URIs, so no need to do anything if you want load base64 strings as data URIs. This code is for example purposes only.

If you did want to implement support for data Uris, you’d probably want to do some more error checking on handles() or in the DataFetcher to handle truncated strings where your indexes might exceed the bounds of the uri.

Advanced use cases

There are a couple of more advanced use cases that don’t fit into our tutorial above. We’ll address them here individually.

Delegating to another ModelLoader

One thing we mentioned earlier, but didn’t discuss in detail is that Glide allows you to delegate to an existing ModelLoader in a custom ModelLoader. It’s not uncommon to have a custom model type that Glide doesn’t understand, but be able to relatively easily extract a model type that Glide does understand, like a URL, Uri, or file path, from the custom type.

With delegation, you can add support for your custom model by extracting the model that Glide does understand and delegating.

For example, in Glide’s Giphy sample app, we obtain a JSON object from Giphy’s API that contains a set of URLs:

/**
 * A POJO mirroring an individual GIF image returned from Giphy's API.
 */
public static final class GifResult {
  public String id;
  GifUrlSet images;

  @Override
  public String toString() {
    return "GifResult{" + "id='" + id + '\'' + ", images=" + images
        + '}';
  }
}

Although we could extract the urls in our View logic and do something like this:

Glide.with(fragment)
  .load(gifResult.images.fixed_width)
  .into(imageView);

It’s cleaner if we can just pass in the GifResult directly:

Glide.with(fragment)
  .load(gifResult)
  .into(imageView);

If we had to re-write all of our URL handling logic to do that, it wouldn’t be worth the effort. If we can delegate though, we end up with a fairly simple ModelLoader implementation:

public final class GiphyModelLoader extends BaseGlideUrlLoader<Api.GifResult> {
  private final ModelLoader<GlideUrl, InputStream> urlLoader;

  private GiphyModelLoader(ModelLoader<GlideUrl, InputStream> urlLoader) {
    this.urlLoader = urlLoader;
  }

  @Override
  public boolean handles(@NonNull Api.GifResult model) {
    return true;
  }

  @Override
  public LoadData<InputStream> buildLoadData(
      @NonNull Api.GifResult model, int width, int height, @NonNull Options options) {
    return urlLoader.buildLoadData(model.images.fixed_width, width, height, options);
  }
}

The ModelLoader<GlideUrl, InputStream> required by our ModelLoader’s constructor is provided by our ModelLoaderFactory which can look up the currently registered ModelLoaders for a given model and data type (GlideUrl and InputStream in this case):

/**
 * The default factory for {@link com.bumptech.glide.samples.giphy.GiphyModelLoader}s.
 */
public static final class Factory implements ModelLoaderFactory<GifResult, InputStream> {
  @Override
  public ModelLoader<Api.GifResult, InputStream> build(MultiModelLoaderFactory multiFactory) {
    return new GiphyModelLoader(multiFactory.build(GlideUrl.class, InputStream.class));
  }

  @Override public void teardown() {}
}

Glide’s ModelLoaders are built lazily and torn down if new ModelLoaders are registered so that you never end up using a stale ModelLoader when you use this delegation pattern. As a result, our GiphyModelLoader is totally decoupled from the networking library we actually use to load the url.

The MultiModelLoaderFactory can be used to obtain any registered ModelLoader. If multiple ModelLoaders are registered for a given type, the MultiModelLoaderFactory will return a wrapping ModelLoader that will attempt each ModelLoader that returns true from handles() for a given model in order until one succeeds.

Handling custom sizes in ModelLoaders

Even with delegation, the Giphy example above might seem like a fair amount of work for a slightly nicer API. However, there are additional benefits to having your own ModelLoader, especially for APIs like Giphy’s where you have multiple URLs you can choose from.

Although we implemented buildLoadData() previously for our base 64 ModelLoader, we never discussed the arguments provided other than the model. buildLoadData() also passes in a width and a height that can be used to select the most appropriately sized image, which can save bandwidth, memory, CPU, disk space etc by only retrieving, caching, and decoding the smallest image necessary.

The width and height passed in to buildLoadData() are either those provided by the Target or, if specified, the override() dimensions for the request. If you’re loading into an ImageView the width and height provided to buildLoadData() are the width and height of the ImageView (again unless override() is used). If you use Target.SIZE_ORIGINAL, the width and height will be the constant Target.SIZE_ORIGINAL.

The actual GiphyModelLoader has a simple example of using the dimensions provided to buildLoadData() to pick the best available url:

@Override
protected String getUrl(Api.GifResult model, int width, int height, Options options) {
  Api.GifImage fixedHeight = model.images.fixed_height;
  int fixedHeightDifference = getDifference(fixedHeight, width, height);
  Api.GifImage fixedWidth = model.images.fixed_width;
  int fixedWidthDifference = getDifference(fixedWidth, width, height);
  if (fixedHeightDifference < fixedWidthDifference && !TextUtils.isEmpty(fixedHeight.url)) {
    return fixedHeight.url;
  } else if (!TextUtils.isEmpty(fixedWidth.url)) {
    return fixedWidth.url;
  } else if (!TextUtils.isEmpty(model.images.original.url)) {
    return model.images.original.url;
  } else {
    return null;
  }
}

In Glide’s Flickr sample app, we see a similar pattern, although somewhat more robust because of the large variety of available thumbnail sizes.

If you have access to an API that either allows you to specify a specific size to request or that offers a variety of thumbnail sizes, using a custom ModelLoader can significantly improve the performance of your application.

BaseGlideUrlLoader

To save some of the boiler plate required to write a custom ModelLoader that just delegates to the default networking library, Glide includes the BaseGlideUrlLoader abstract class. A couple of our previous examples, including the GiphyModelLoader and the FlickrModelLoader make use of this class.

BaseGlideUrlLoader provides some basic caching to minimize String allocations and two convenience methods:

  1. getUrl() which returns a String url for a given model
  2. getHeaders() which can be optionally implemented to returns a set of HTTP Headers for a given model and dimensions if you need to add an authentication or other type of header.

getUrl

If you read the earlier section about handling custom sizes, you might have noticed that the method in GiphyModelLoader we referenced isn’t actually buildLoadData(). It’s actually just the getUrl() convenience method:

@Override
protected String getUrl(Api.GifResult model, int width, int height, Options options) {
  Api.GifImage fixedHeight = model.images.fixed_height;
  int fixedHeightDifference = getDifference(fixedHeight, width, height);
  Api.GifImage fixedWidth = model.images.fixed_width;
  int fixedWidthDifference = getDifference(fixedWidth, width, height);
  if (fixedHeightDifference < fixedWidthDifference && !TextUtils.isEmpty(fixedHeight.url)) {
    return fixedHeight.url;
  } else if (!TextUtils.isEmpty(fixedWidth.url)) {
    return fixedWidth.url;
  } else if (!TextUtils.isEmpty(model.images.original.url)) {
    return model.images.original.url;
  } else {
    return null;
  }
}

Using BaseGlideUrlLoader allows you to skip constructing the disk cache key and LoadData and allows you to avoid dealing with delegation, aside from the ModelLoader<GlideUrl, InputStream> you have to pass in to the constructor.

getHeaders

Although Glide’s sample apps don’t need to use getHeaders(), it’s not uncommon to have to attach some form of authentication when retrieving non-public images. The getHeaders() method can be optionally implemented to return any set of HTTP headers that is appropriate for a given model.

For example, if you had a string authorization token, you might use the LazyHeaders class to write something like this:

@Nullable
@Override
protected Headers getHeaders(GifResult gifResult, int width, int height, Options options) {
  return new LazyHeaders.Builder()
      .addHeader("Authorization", getAuthToken())
      .build();
}

If your getAuthToken() method is especially expensive, you should use the LazyHeaderFactory instead:

@Override
protected Headers getHeaders(GifResult gifResult, int width, int height, Options options) {
  return new LazyHeaders.Builder()
      .addHeader("Authorization", new LazyHeaderFactory() {
        @Nullable
        @Override
        public String buildHeader() {
          return getAuthToken();
        }
      })
      .build();
}

Using LazyHeaderFactory will avoid running expensive calls until the HTTP request is made in the DataFetcher. Although the ModelLoader methods are called on background threads, buildLoadData() is called, even if the corresponding image is in Glide’s disk cache. As a result, it’s wasteful to perform expensive work during the buildLoadData() method or any of the BaseGlideUrlLoader methods because the result may noto be used. Using LazyHeaderFactory will defer the work, saving a significant amount of time for expensive to acquire headers.

Credits

Thanks to jasonch@ for Glide’s data uri ModelLoader implementation.