Displaying images in Android app: maintainable, testable, painless. Part I
If your app shows dozens of thousands of images, these images would most likely come from various sources: downloaded from the web, loaded locally, generated, etc.
Writing additional code for displaying every type is time-consuming, inefficient, not scalable and potentially causing a lot of pain and bugs.
Every time you add a new screen you will get swamped copypasting your essential code and swamped again fixing the bugs you created while copypasting.
What you really need instead of suffering this agony is a slick and elegant system to centralize the whole displaying process and fit the maintainable, testable and painless criteria.
Here is how we did it in Revolut’s Android app.
The article is divided into two parts:
- Part I — improving the legacy approach and building delegates,
- Part II — making transformations, testing and summing-up the results.
Revolut app
There are several types of image use in Revolut app:
- Transaction lists with various icons,
- Cards lists,
- Lottie animations,
- GIFs.
Let’s take a look at the transactions list. It may contain dozens of cell types, however, we’ll pick five of them as an example.
Each image has its own source or can be generated.
The legacy approach
Following the legacy approach to displaying the list you would start with creating the adapter:
That’s how a standard adapter for RecyclerView looks like. Binding would be the next step here:
We get a long list of conditions because every type of transaction has its own display logic in the adapter. It can be even more complicated if we use a separate ViewType for each type (it is also led by the adapter contract):
As we know, there might be dozens of transaction types, and we can’t use this approach to build the adapter.
Improving the adapter
There are two basic approaches to the adapter’s extension — ViewType and delegates.
The ViewType approach can be used when the app is simple and contains only one list or a couple of screens. This is not our case because such an adapter can’t be reused. If we continue to extend the adapter and to add new ViewTypes, the adapter will constantly grow. Moreover, we’ll have to build new adapters for each screen in the app.
The delegates approach seems to be easier: we don’t need separate adapters for every screen. Four years ago Hannes Dorfmann described the approach, and today you may easily find a library for its implementation. We’ll use Dorfmann’s library.
Take a look at the simple delegate that displays ProgressBar:
We create a ViewHolder in the delegate as if we did it in standard adapters. After that, we go for binding. The main difference is that each delegate has its own model which will be used to display the cell type needed. Also, each model implements interface ListItem with the listId field and calculatePayloads method.
Let’s create an adapter that can display delegates:
As you may have noticed, we can easily use the ListItem interface within the ListDiffCallback class, so that DiffUtil doesn’t refresh unchanged cells and doesn’t launch extra animations. Besides, as we use data class for models, equals are available out of the box. All we need to do with DiffUtil is to create the right model of the delegate.
The adapter for every screen is being created by declaring a list of delegates that the screen must support:
Displaying images
We remove the logic of loading and displaying images from the adapter, and we ease onBindViewHolder. Basically, we need to create two things — an image model and the delegate which will be able to load and display the image. Here is the example of a model where we load the image from sources:
First, we build the interface Image. Then we describe a set of parameters for ResourceImage, that can be used to set the image. Particularly — image resource id and colors, if we want to paint it over.
After that, we move to delegate and create its interface. You can see why we need the interface Image.
Each delegate must be able to:
- understand if it can display the image or not,
- display the image in ImageView.
Loading images from resources will look like this:
You can see that:
- Method suitsFor verifies that image is ResourceImage,
- We set image in ImageView within the displayTo method, and if colorRes is not null, we set tint.
It’s the simplest of all possible delegates.
Combining delegates
It’s about time to combine all supported delegates in one place and to cut down the interface to the method displayTo.
Check the highlighter line. Using the first method we find the first suitable delegate to display the image. If it’s not found, there will be a crash. And it’s not an error in the architecture: we intentionally use the fail-fast approach to get rid of unobvious behavior. Otherwise, if the image is not displayed, it’s hard to tell what’s causing the issue.
Stay tuned for the second part of the article to be released next week.