Displaying images in Android app: maintainable, testable, painless. Part II

Eugene Zubkov
Revolut Tech
Published in
6 min readJul 31, 2019

--

Last week we published the first part of this article about building maintainable and scalable system for displaying dozens of thousands of images in your Android app.

Part I contained the approach description and the delegates building instructions. The second part describes the transformation and testing processes and sums-up the results.

Please check the first part before you start reading. Let’s go.

Making transformations

Why do we need transformations? Let’s pretend that we have a contact’s (or a merchant’s) userpic uploaded from the web. The userpic can have any form and size, whilst in Revolut app, we need to display it round-shaped at 40 х 40 dp.

How we display avatars in Revolut app

To set the model and make it behave accordingly let’s take UrlImage class as an example. Every image to be transformed should have relevant settings. We set it by creating TransformableImage interface with transformations property:

The class may look like this:

To display images, we use Glide. Naturally, all our transformations correspond to this library.

That’s how we create the transformations array.

To avoid extra work, mark the fields as nullable (this lets you implement only the needed transformations), and pay attention to the transformations’ order.

Imagine that we have a very large image we need to rotate, scale and crop into a circle. Let’s compare two scenarios:

In the first one we start with image rotation, then crop it into a circle and only after that we scale it. In the second scenario, we start with scaling.

Obviously, the second one seems more reasonable, because rotating and cropping of a small image demand fewer hardware resources.

Earlier we created the array that we’ll pass to Glide when it displays an image by URL. Now we build an object RequestOptions and pass the array to the object. If we pass an empty array, Glide will fail. So it is mandatory to add a verification.

We’re going to reuse transformations in different delegates, that’s why we can put them to the extension method applyImageTransformations.

Also, we add a method to the interface TransformableImage — getGlideTransformsArray. The interface and the extension method applyImageTransformations are marked as internal. It helps to avoid the leak of abstraction — Glide is not a part of the public interface. It’s important if we want to change Glide to any other library.

The code looks like this:

Creating a delegate to display transaction

We already know how the adapter works. Let’s create a delegate to display the transaction. The basic version looks like this:

To simplify the code we skip displaying the text. We can make this delegate display transactions with pictures from web or resources, with contact userpic generated from the initial letters.

We start by modifying the model.

In each case, we use specific parameters gathered in one place. The image will be displayed like this:

This solution has some disadvantages:

  • hard to extend;
  • the order is important, and it may not be obvious which order to choose;
  • the logic is inside the adapter (in the delegate).

Let’s try to use image delegates. In the model, we leave only the image to display instead of all other parameters.

The transaction list will look like this:

Its behavior becomes more obvious, and also we take the logic out of the adapter.

Creating a delegate for generated images

Let’s take a particular case of creating a delegate that generates an image of two symbols. First of all, we define requirements for this delegate: it must be able to display the letters and adjust the image.

Hence, the model looks like this:

To adjust the background we use ImageTransformations.

Now let’s proceed to bitmap generation. For example, we can use TextDrawable, where the image is built with Canvas. Then we handle the bitmap and set it in ImageView.

As we use the extension, the delegate takes only a couple of lines. Here is how it works.

The first version with basic settings is:

At the second stage, we crop the image into a circle:

At the third stage, we rotate the image. So we can display the userpic icon as we need to follow the design guidelines.

Creating customized transformation

Let’s say, we need to flip the image horizontally. In order to do so, we build a transformation class framework.

If we use Glide, the basic class is BitmapTransformation: Glide makes our life easier because it contains TransformationUtils with methods we need. All we have to do is to add the transformation to others.

Testing

One of the main reasons why we use this solution is testability.

Let’s see how the clean architecture may look like, and see how data goes to the UI layer. We use the transaction list as data.

Clean architecture

It looks pretty regular. The database returns the models list, and on the repository level, we map them into models of the domain level. Then it sends them to UI level. Each mapping stage is covered with tests.

This is how the domain transaction’s model looks like:

It contains the transaction’s id, amount and date. How does it understand if it’s a money transfer or a purchase? Where does it take the title or URL? The answer is the sealed class.

We see two transaction types — money transfer and purchase. Each of them has a unique set of parameters.

What is the model for the UI layer? Let’s go back to the delegate for the adapter RecyclerView.

Delegate model can be perfectly used as a UI model.

Here are some cases we can test only with the help of delegates.

Case 1 — Money transfer to a contact without a userpic.

We expect that if there is no URL for userpic, the model to display the initial letters is created.

Case 2 — Money transfer to a contact with the userpic.

We expect that UrlImage with a transformation will be created.

Case 3 — Purchase in a shop with the userpic.

Same as the second: we expect that UrlImage with a transformation will be created.

Case 4 — Purchase in a shop without a userpic.

Here we can make an additional test: every purchase can belong to a separate category, so the icons will differ. We can also check if we map categories in the relevant icons.

Conclusion

We benefit from using delegates.

First, we remove the logic that shouldn’t be in the adapter: it shouldn’t be responsible for the image source choice, depending on its parameters.

Secondly, we don’t depend on image loading and adjusting the method anymore. We can replace Glide with any other library at any moment.

Thirdly, we can check that image type we need is displayed, in other words, we can test the data displaying.

--

--