At Novoda, we always experiment new ways to improve the user experience of the apps we craft. Contrary to what one would normally believe, though, the user experience begins even before an app is installed.

There are some important things to do to ensure your app is successful, starting at the very first moment a user sees your app on the store. Curate the listing for the app so that it makes it stand in the (over?)crowded market. Make the installation process as smooth as possible.

First impressions do matter!

We'll see how we changed our process for one of our apps to improve the user experience even before a user gets to its first screen.

Growing pains

##### A big APK has a bad install UX. The install phase alone can take minutes.

As apps become more complex and screens get more pixels, the APKs get bigger. Their size has been constantly growing over the years since Android was released, back in 2008. Nowadays, it's rather common to have apps weighing in at 15, 20 or even 50+ megabytes. Apps that do many things need more code, more libraries, more assets for all the features. Also, the ever growing screen density requires higher and higher resolution assets. You see where this is going.

If you unpack an APK and look at what is taking up space, you can see that there are usually the same three items in the top spots: one or more classes.dex , the resources.asrc and the res folder. Sometimes the assets folder can contain a lot of stuff as well.

Code in the dex file(s) has an impact on the final APK size, but reducing its size is a discussion for a whole other post.

The compressed resources file ( resources.arsc ) size depends directly on how many resources and how many configurations are in the APK. Optimising it is a rather advanced topic; there are some tools that can help, if you're interested, but it's likely you won't need them.

What's left then to optimise? Your resources. And what are the easiest ones to optimise? Drawables!

Solutions? Errm, workarounds

There are several ways to address the ever-growing size of assets in an APK. Some are very effective, but bring caveats with them. Others are quicker wins but might be less effective. Let's look into some of them.

APK Splits

One way to generate smaller APKs is to have the build tools create density-specific APK splits for you. It's easy to set this up and even create architecture-specific APKs if you have native libraries. The Play Store and the Amazon Appstore make it effortless to distribute the right APK to the right device, basically taking care of everything.

You'll end up with a lot of APKs to test, maintain, version and archive. It can be very awkward to handle all of them, considering they grow in number almost exponentially with the number of split dimensions. Testing all the resulting APKs can be time consuming, especially if manual QA is part of your process.

WEBP

Another way to reduce the size of the APK resources is to use WEBP, an open-source image format created by Google drawing from the VP8 codec, that is best known for its use in the WEBM codec. Why use WEBP, you ask? Well, for starters it's much more efficient than the dated JPG for lossy images, but its lossless variant is also more efficient than PNG.

$ ls -l image-test/ 39610 24 May 16:45 carota.png 19031 24 May 17:09 carota-optim.png 5868 24 May 17:03 carota.webp

The same image in straight-out-of-Photoshop PNG, optimised PNG, and converted into WEBP using cwebp

Google has done some rigorous research on the performance of lossy WEBP vs JPG and lossless WEBP vs PNG.

So why doesn't everyone immediately jump on the WEBP bandwagon, since it's so clearly more efficient? Well, it's a two-fold issue. Firstly, it's a matter of compatibility. Lossy WEBP is only supported starting from Android 4.0, and lossless WEBP support was introduced in Android 4.2.1. As an aside, WEBP images can't be used for launcher icons.

The first requirement isn't too restrictive, as hopefully very few apps are still supporting devices on API 13 or lower. The API 18 requirement, on the other hand, is way harder to manage. That's unfortunate, given lossless WEBP is probably the most interesting format of the two for drawables, as PNG is the most used format for assets.

There is not much support for the WEBP format in content creation software, either. Photoshop, Illustrator, Sketch, and most other applications don't support exporting to WEBP out of the box. You must add a step in your design pipeline to convert images using cwebp , imagemagick, a Photoshop plugin or some other tool. Designers aren't used to this format and its adoption might encounter some resistance.

Using WEBP for images is still strongly recommended, if you can. Try using it at least for transmitting lossy images over the network, as they will allow either a much smaller size compared to JPG for the same quality, or a higher visual quality at the same size. This works down to API 14, which should meet most apps' minSdkVersion anyway.

Vector Drawables

Since a lot of the images used in apps tend to be simple, monochromatic icons, we can include them in vector format instead of raster. Lollipop introduced support for vector drawables, including animating them. Vector drawables are based off of a subset of SVG path commands, and as such are relatively straightforward for designers to produce (most tools support exporting as SVG, and there are conversion tools to get the final vector drawable).

Since the release of Support Library 23.2.0, we've even had an official way to use them in previous versions of Android (other libraries existed before for the same purpose, but none was as feature complete, nor officially supported). Unfortunately, a memory leak was quickly discovered that prompted Google to rush out a release to disable them completely. Google engineers have since restored access to the support vector drawables in 23.4.0, but with some pretty strong limitations on how and when you can use them.

Manually shrink drawables

The last possibility is to use a tool to reduce the size of images before we add them to the res folder. One such tool is ImageOptim, which is rather efficient but only runs on Mac OS X. Several alternatives exist for Mac OS and for other platforms, both free/FOSS and paid.

These tools run a series of algorithms in order to remove overhead from the files, optimise their compression levels, and even recompress the data streams for maximum efficiency. ImageOptim, for example, can turn a 24 bpp PNG into an 8 bpp one, strip all metadata, optimise the palette, and recompress the stream using Google's Zopfli algorithm.

You'll remember that APK we looked into at the beginning of the post. That is the latest Google+ APK at the time of writing. It looks like no optimisation has been done on its drawables. How much would we save if we ran ImageOptim on it?

And it wasn't even done crunching—it's a time-intensive task, as mentioned before.

Pretty. Darn. Impressive.

Why not just using the built-in AAPT PNG cruncher? Well, besides not supporting JPGs, the AAPT cruncher only does a few simple optimisations that in and by themselves won't likely save you much. Watch Colt McAnlis' talk on image compression at Google I/O 2016 for more details (link at the bottom).

Build a process

Running an image optimiser is yet another step required to deliver drawables. How does it fit into our process? Let's explore the alternatives.

Whose job is it?

If you decide to manually compress assets as they are about to be added to the app, it's very important that roles are clearly defined. Optimising images before dumping them in the APK is an important job, given we'll have to give up AAPT's built-in shrinker (more on that later). You must decided whether it's the designer's or the developer's job.

You might wonder, why would anyone decide not to automate it? Well, if you only have a trickle of new assets coming into your app every now and then, automation might be overkill, or simply waste time (optimising a lot of images takes aLot^n time).

In one of our projects, developers and designers sat down to decide how to integrate optimisation into the process, and designers volunteered to take care of this themselves. We have awesome designers! ✨

Continuous Integration

An obvious idea is to automate optimising images. After all, you don't want to rely on humans to do repetitive tasks, right? Also, there's no risk someone forgets something, and nobody will have to think about it anymore.

When could it be done? Setting up a cron on the CI server to compress all the assets, or do it on every CI build, might seem like a good idea. Unfortunately, it's often impractical to try to run image compression on all the assets you have in the app, as that process can take a significative amount of time. These algorithms often work on a "try until you can't get any improvement, or I say you've spent enough time on it" basis. Others, like Zopfli, are simply time consuming in nature. A full run might last anywhere from few minutes to several hours, depending on a variety of factors.

That's why going the CI way is an option only if you change your assets very often, and you can spare the extra time it requires or have a very beefy CI.

⚠️ Optimisations aren't idempotent ⚠️

One very important caveat to both the aforementioned processes is that optimisation passes aren't necessarily idempotent. This means that if you run an image through an optimiser such as ImageOptim, and then run it through another one, the final image size might actually be bigger than the original one. This happens because re-processing images might end up undoing the optimisations done in the first pass, depending on the tools you use.

Take for example AAPT, the Android APK Packaging Tool. It's the piece of the build toolchain responsible for packaging up APKs, handling resources, and so on. Unbeknownst to many, it also offers some basic crunching capabilities for PNG (and PNG only!) images. It's enabled by default, but you will want to disable it. Because of a regression (that should be fixed in the near future), it will try to recompress—badly—the images, effectively bloating them.

In our tests, we noticed that leaving the aapt cruncher enabled can even make your final APK size bigger than the one you started with. Not good. Let's hope this issue is fixed soon, so we can even remove this step and make things even easier.

To disable the AAPT cruncher, put this in your build.gradle file:

aaptOptions { cruncherEnabled = false }

Note: this won't exclude 9-patch pre-processing

Colt McAnlis goes a bit more into details on the matter on Medium. I will keep it short and simple, here.

Conclusions

Put your APK on a diet and you'll make people happy. Stop the seemingly unstoppable growth trend that APKs have seen for years, with very little effort. A great user experience begins with not having to wait for ages for your app to download and install!

Acknowledgements

I want to thank Wojtek Kaliciński from Google, for the great talk he gave a few months ago at Londroid, our monthly Android meetup in London. That talk contained a lot of super useful information that goes beyond the scope of this article, but also inspired me to look into shaving off the fat from our APKs. I recommend you watch his I/O talk, based on the posts on Medium and on the Londroid talk.

Thanks to Mike Wolfson and Rui Teixeira for their help with editing and their suggestions.

Further reading/watching

Here's a few resources you can refer to if you want to go deeper down the rabbit hole: