Modularisation is currently a hot topic in the Android development world. Applications are now migrating to a modularised structure in order to achieve faster Gradle builds, reuse code across functionalities and to create smoother Git flows by splitting the team between the different modules.

However, this concept is also applicable when developing a library, for slightly different reasons. In case you use Google Play Services in your Android development, you probably know you shouldn’t include the whole com.google.android.gms:play-services:x.y.z but instead target only the modules you actually need, e.g. play-services-analytics or play-services-maps , or you’ll see your app size grow a lot!

Here at Onfido, our modularisation journey started with a similar problem.

Why? 🤔

Our problem was that our Android SDK size was growing too much, mostly due to the image quality validation features we brought to the device to help us reduce input quality failures on uploaded documents, in the form of C++ native code, facilitated by Java Native Interface (JNI).

We started receiving messages from some clients with feedback about the size of their apps after upgrading to a new version that contained these features. These messages came from clients whose apps usage is widely spread across the globe, including many developing countries, meaning they must be as small as possible to fit network and storage constraints on low-end devices.

Ok… But why is C++ code so big?

The main reason is that we use OpenCV, a computer vision and machine learning library that helps us manipulate the images coming from the device camera and apply our research algorithms on them to get these image validations we talked about earlier. OpenCV has quite a big size impact which will then be reflected in the size of our SDK. For the C++ native code, there is no Proguard to help us shrink our code, and the compiler flags that tend to do the same job didn’t prove effective in our use case.

Also, since Android works on a wide range of devices, with different CPUs, the native code must be compiled in multiple architectures. It is the responsibility of the host application to perform a split of its APK based on these architectures, to avoid including repeated native libraries on one single APK.

ABI filters for native code

The figure below shows the size of a baseline sample app where the only dependency is our 4.0.0 version, and for size calculation we should look at the first 2 architectures, since 99% of the devices will fall under those 2 architectures, being the 3rd one an APK that works on every device and emulator, which means it has compiled native libraries for all those architectures inside, and the last two are mostly used by emulators and not physical devices.

ABI-dependent sizes of our Android SDK

So given the big size of our native library, our problem was identified and we started drafting the solution with some requirements in mind.