GitHub: Unified image loading architecture

preface

It’s Glide, Picasso or Fresco, which is basically the mainstream image loading framework, and most of the time we use it, we feel like we have a finger in the arm, we feel like we don’t want to use it. First, when the requirements change and you need to add a separate placeholder for when the image load fails, you have to add it to every place where Glide is used. Second, when you need to refactor your project, or when your current image-loading framework doesn’t meet certain requirements and needs to be replaced, you may still need to overhaul the original project.

If you look at your code, do you all have this kind of problem? When I see the code for my team’s project, my head is always twice as big as usual… Why do you ask me? For example, Glide was directly used in the project at the earliest. Later, I suggested that at least a little packaging should be made. After all, it should not be too ugly to eat, so I made a layer of packaging, but still could not withstand the test of new demand. Not to mention the extent to which frameworks were replaced (which is probably why our team switched to RN, since no one wanted to look at old code anymore).

If you think this is because I’m a perfectionist, then I probably haven’t tried to paste and copy line by line and delete refactoring days.

With that said, let’s just get started

Encapsulation’s new mission

Let’s talk about encapsulation, the benefits of encapsulation we are familiar with, external to provide a simple interface shielding internal complex, protect data, ensure security…. Why? Because all of our frameworks, such as OKHTTP, Retrofit,Glide, and so on, have been perfectly encapsulated and have been able to provide simple interfaces externally and mask the complexity internally. Data protection, security and so on, if only for those purposes, we don’t need to do encapsulation.

So what is the new mission that we’re encapsulating, in order to achieve control over the module, what does that mean? If you use Glide,Picasso, or Fresco directly in your business code, that means you give them complete control over the image loading process, and you need to change the image loading process one by one. Then you lose control of the image loading module. So, what I mean by control over modules is that you can modify or even replace the entire module at any time with very little cost.

This is why we still need to encapsulate development frameworks when they have encapsulated themselves so well.

All right, now, let’s look at the specific problem.


Start with the package Glide

Glid, for example, uses a chain of calls to call various image-loading related Settings, such as cache policy, animation, placeholders, and so on. There are countless kinds of apis, and now we need to abstract these calls into an interface, so that it can be easily encapsulated.

A simple Glide call might look like this:

        Glide.with(getContext())
                .load(url)
                .skipMemoryCache(true)
                .placeholder(drawable)
                .centerCrop()
                .animate(animator)
                .into(img);Copy the code

Although Glide doesn’t have all of its image loading Settings, you should get the sense that its image loading Settings are very rich and arbitrary. So how do we package it into an interface? Probably the first thing that comes to mind is this:

public interface ImageLoader{
    static void showImage(ImageView v, Context context,String url, boolean skipMemoryCache,int placeholder,ViewPropertyAnimation.Animator animator)
}Copy the code

This is obviously problematic, as encapsulating an interface with many options requires preserving the richness of options while ensuring uniform and concise invocation. Such a long list of parameters clearly hurts.

So how should design? From this perspective, we can analyze what is the most basic and important option for image loading, and what is optional:

  • Mandatory options: URL (image source), ImageView(image container), Context
  • Optional: Everything except the required option

So our interface is starting to take shape

public interface ImageLoader{
    void showImage(ImageView imageview, String url, Context context,ImageLoaderOptions options);
    void showImage(ImageView imageview,int drawable,Context context,ImageLoaderOptions options);
}Copy the code

Is that all right? No, we can keep exploring, and we find that the ImageView actually contains the Context parameter, which can be omitted, so our basic parameters should be: URL,ImageView,options,

public interface ImageLoader{
    void showImage(ImageView imageview,  String url, ImageLoaderOptions options);
    void showImage(ImageView imageview, int drawable,ImageLoaderOptions options);
}Copy the code

And then let’s look at the ImageLoaderOptions defined in this method. This is actually quite simple. Basically, Glide has as many options as you can add attributes to it. Since these properties are optional, we need to use the Builder pattern to build it without going into details.

So, here, we for Glide packaging design is basically completed.


Unified image loading architecture

We said we wanted to build a unified image loading framework, which means that Glide, Fresco, Picasso can play with it. In fact, we just need to make a further improvement on the package Glide, because when we package Glide, it is already an abstraction of image loading.

The abstract interface is generally available in other image loading frameworks, but Fresco’s design of implementing the image container itself caused a bit of a problem, but it’s easy to use the View as the image container in the interface.

public interface ImageLoader{
    void showImage(View v,  String url, ImageLoaderOptions options);
    void showImage(View v, int drawable,ImageLoaderOptions options);
}Copy the code

Ok, so this interface is basically perfectly compatible with Glide, Picasso, Fresco, all three loading libraries. Now the question is how to make them replaceable. At this point we need a design pattern (strategic pattern can’t wait to jump out and say, pick me, pick me!).

That’s right, the strategy mode, which looks like this:


So far, we have completed a unified image loading architecture design, but there is one problem I have saved for last, which is the internal structure of ImageLoaderOptions.

When you want to wrap a Glide, ImageLoaderOptions can match the Settings in Glide exactly, and you can put all the image-loading Settings in Glide if you want. But can we do this if we want to accommodate three loading frameworks or more?

In theory, yes, but when you do, ImageLoaderOptions might look something like this:

Public class ImageLoaderOptions {// private int green placeHolder=-1; Private ImageReSize size=null; private ImageReSize size=null; Private int errorDrawable=-1; private int errorDrawable=-1; Private Boolean isCrossFade= drawable when loading errorfalse; Private Boolean isSkipMemoryCache =false; . / / whether to skip the memory cache private ViewPropertyAnimation Animator Animator = null; // Picture loading animation... . Private int placeHolder= placeHolder; private int placeHolder= placeHolder; Private Drawable pressedStateOverlay =null; // Layers displayed when pressed private Boolean isCrossFade=false; // Whether gradient smooth display image... . }Copy the code

It’s easy to see how many Settings overlap between image loading frameworks, such as placeholders, progressive loading, caching, etc. Some Settings are similar, so we should actually merge them together. In other words, when we think about the design of ImageLoaderOptions, We should start by combining several frameworks with common and similar Settings, as this represents the most common and important requirement in the image loading area. Then we added the Settings that we needed to differentiate between frameworks as needed.

Below is my specific implementation of this unified image loading architecture for your reference only.

The interface definition

public interface ImageLoaderStrategy{
    void showImage(View v,  String url, ImageLoaderOptions options);
    void showImage(View v, int drawable,ImageLoaderOptions options);
}Copy the code

Setting Item Definition

Public class ImageLoaderOptions {// You can import all the common or similar Settings of the three image-loading frameworks. private int placeHolder=-1; Private ImageReSize size=null; private ImageReSize size=null; Private int errorDrawable=-1; private int errorDrawable=-1; Private Boolean isCrossFade= drawable when loading errorfalse; Private Boolean isSkipMemoryCache =false; . / / whether to skip the memory cache private ViewPropertyAnimation Animator Animator = null; Private ImageLoaderOptions(ImageReSize resize, int placeHolder, int errorDrawable, Boolean isCrossFade, boolean isSkipMemoryCache, ViewPropertyAnimation.Animator animator){ this.placeHolder=placeHolder; this.size=resize; this.errorDrawable=errorDrawable; this.isCrossFade=isCrossFade; this.isSkipMemoryCache=isSkipMemoryCache; this.animator=animator; } public class ImageReSize{ int reWidth=0; int reHeight=0; public ImageReSize(int reWidth,int reHeight){if (reHeight<=0){
                reHeight=0;
            }
            if (reWidth<=0) {
                reWidth=0;
            }
            this.reHeight=reHeight;
            this.reWidth=reWidth;

        }

    }
 public static final  class Builder {
        private int placeHolder=-1; 
        private ImageReSize size=null;
        private int errorDrawable=-1;
        private boolean isCrossFade =false;
        private  boolean isSkipMemoryCache = false;
        private   ViewPropertyAnimation.Animator animator = null;
        public Builder (){

        }
        public Builder placeHolder(int drawable){
            this.placeHolder=drawable;
            return  this;
        }

        public Builder reSize(ImageReSize size){
            this.size=size;
            return  this;
        }

        public Builder anmiator(ViewPropertyAnimation.Animator animator){
            this.animator=animator;
            return  this;
        }
        public Builder errorDrawable(int errorDrawable){
            this.errorDrawable=errorDrawable;
            return  this;
        }
        public Builder isCrossFade(boolean isCrossFade){
            this.isCrossFade=isCrossFade;
            return  this;
        }
        public Builder isSkipMemoryCache(boolean isSkipMemoryCache){
            this.isSkipMemoryCache=isSkipMemoryCache;
            return  this;
        }

        public ImageLoaderOptions build() {return new ImageLoaderOptions(this.size,this.placeHolder,this.errorDrawable,this.isCrossFade,this.isSkipMemoryCache,this.animator);
        }
    }Copy the code

This interface is implemented by Glide:

public class GlideImageLoaderStrategy implements ImageLoaderStrategy {

    @Override
    public void showImage(View v, String url, ImageLoaderOptions options) {
        if(v instanceof ImageView) {// Convert the type to ImageView; DrawableTypeRequest DTR = Glide. With (imageView.getContext()).load(url); DrawableTypeRequest DTR = Glide. LoadOptions (DTR, options).into(imageView); } } @Override public void showImage(View v, int drawable, ImageLoaderOptions options) {if(v instanceof ImageView) { ImageView imageView= (ImageView) v; DrawableTypeRequest dtr = Glide.with(imageView.getContext()).load(drawable); loadOptions(dtr, options).into(imageView); }} private DrawableTypeRequest loadOptions(DrawableTypeRequest DTR,ImageLoaderOptions options){if (options==null) {
            return dtr;
        }
        if(options.getPlaceHolder()! =-1) { dtr.placeholder(options.getPlaceHolder()); }if(options.getErrorDrawable()! =-1){ dtr.error(options.getErrorDrawable()); }if (options.isCrossFade()) {
            dtr.crossFade();
        }
        if (options.isSkipMemoryCache()){
            dtr.skipMemoryCache(options.isSkipMemoryCache());
        }
        if(options.getAnimator()! =null) { dtr.animate(options.getAnimator()); }if(options.getSize()! =null) { dtr.override(options.getSize().reWidth,options.getSize().reHeight); }returndtr; }}Copy the code

The Picsso,Fresco interface implementation class follows Glide.

The following is the last step, the implementation of the entire image loading architecture management class, used to provide external image loading services and image loading framework replacement

public class ImageLoaderStrategyManager implements ImageLoaderStrategy { private static final ImageLoaderStrategyManager  INSTANCE = new ImageLoaderStrategyManager(); private ImageLoaderStrategy imageLoader; privateImageLoaderStrategyManagerGlide imageLoader=new GlideImageLoaderStrategy(); } public static ImageLoaderStrategyManagergetInstance() {returnINSTANCE; } // Can replace the image loading frame public voidsetImageLoader(ImageLoaderStrategy loader) {
      if (loader != null) {
          imageLoader=loader;
      }
   }

    @Override
    public void showImage(@NonNull View mView, @NonNull String mUrl, @Nullable ImageLoaderOptions options) {

        imageLoader.showImage(mView,mUrl,options);
    }


    @Override
    public void showImage(@NonNull  View mView, @NonNull int mDraeable, @Nullable ImageLoaderOptions options) {
        imageLoader.showImage(mView,mDraeable,options);
    }

}Copy the code

At this point, the whole picture loading architecture has been designed, we can also basically realize the control of the picture loading module.

Isn’t this little image-loading architecture perfect? No, because of Fresco, when we switch to Fresco, or from Fresco to another loading framework, we may still need to modify the image container node (ImageView/DraweeView) of the XML file, because Fresco uses its own components. However, ONE solution I have considered is to merge the image container (ImageView/DraweeView) nodes into a separate XML file. And in the code level so that the unified View to obtain the image container (ImageView/DraweeView) instance to do the corresponding operation.


The project has been uploaded to Github. Click here to get it.

Afterword.

If you really don’t understand the concept of control, you can also think of it as building highly cohesive, low-coupling modules