How we reduced our Android app size by 55% using Dynamic Delivery

We observed over 55% reduction in the total app size

Khabri
Khabri Technologies
7 min readJun 23, 2022

--

by Kaustubh Patange: (Twitter)

Khabri is an audio platform & we target a very specific audience who may be using some low-end devices or has low bandwidth. Considering this factor, optimizations related to startup, runtime performance & smaller app size are some of the major tasks we do at Khabri. For a long time were focused on delivering rich features for our users, many of the times due to time constraints we have to ignore these optimizations at some level (specifically app size).

We’ve two apps (Main & Creator). In a nutshell, Creator “Khabri Studio” is a platform where a creator can upload their content on their channel could be an Audio, Course (for teachers), etc. & monetize it. Those who want to consume this content will be using the Main “Khabri” app. We offer many features but the one that used to take a lot of app size is the “Live” feature. The concept is very simple, a creator will go live from the Studio app & content consumers will join live through the main app. We use Agora SDK for this feature which is a Real-Time engagement platform where people can see, hear & interact with each other. The products we mainly use are Real-Time Messaging & Live Voice Calling.

One of the problems (or technically not really a problem) with these SDKs are they use a lot of native calls for talking to the system through JNI. Agora claims they denoise the audio in the voice calls, they support a lot of audio formats/visualization, etc. For each such feature a compiled native library (.so) is added per device architecture (armeabi-v7, arm64-v8a, x86, x86_64) to the apk, so suppose the compressedlibagora-rtc-sdk.so takes 4 MB of size for one architecture, then a total of 4x4=16 MB for all architectures if we were to distribute the app in an apk format.

Luckily, since the introduction of app bundles (which is one of the best things that happened in the Android ecosystem) we can split by ABI & reduce the app size to specific device architecture meaning Google Play will deliver only the required libs that are supported by the device during “downloading” phase. This will reduce the overall download size. But the problem still remains, we still have these native libraries which are taking up about ~8 MB of space.

Understanding the target audience

To give fundamental insight into our app, here is the size distribution of the app based on file types for an arm64 device.

Insights captured using Spotify Ruler

As you can see the download size of these libraries is 12.9 MB if compressed they go over 9 MB. This is still huge compared to the total download size of the app which used to be 19-20 MB.

Let’s breakdown these insights into individual components,

Insights captured using Spotify Ruler

From above we clearly noticed that most of the app size was due to these native agora libraries, we had to do something. Remember what I said above “Android App Bundles is one of the best things that happened in the Android ecosystem”, well this is where it shines. In IO’18 along with Android App Bundles, Google announced an entirely new dynamic delivery framework called dynamic feature modules. Unlike Gradle modules, these are special, where users can download your app’s dynamic features on-demand, instead of during the initial install, further reducing your app’s download size. This was perfect for us.

The “Live” feature is where our major audience lives, though there are some users who don’t use this “live” feature much often. For those who use it, we don’t want to reduce their experience when delivering this feature as a dynamic module. Through several internal discussions, the team came to a point where we got a green card to implement this feature at one level. For eg: on some organic users & cases, we must this ship this module during install time, for that we have an optimization put in place that installs this module during onboarding.

This will not be an article on how to implement the solution but rather what benefits it provides & of course with benefits there must be some cons which will be discussed.

Time to shine with Dynamic Delivery

Since we are looking to download the feature at a later period in time when a user asks for it, we implemented On-Demand Delivery. The docs are straightforward & illustrate step-by-step implementation.

One of the things you learn as a software engineer is “reusability”. We’ve OOP principles like SOLID (by Uncle Bob) & great language which is statically typed. Things like abstraction, polymorphism, etc. which these principles elaborate on are one of the things you should keep in mind to improve the structure of your code. Any feature or any piece of code you right should not escape or cause any architecture damage to the code in any way like causing review hells, etc.

The same goes for this feature, we knew from the start we are going to segregate a lot of features through Dynamic delivery so we wrote code which is abstract, are open to extend but close for modification. We’ve got an abstract class ModuleInstaller which implements all the basic code needed to download, install & run the delivered dynamic module. The first step we did is took the live feature & moved it into a separate Gradle module with some additional configuration to make it dynamic i.e by applyingcom.android.dynamic-feature plugin. Once the feature is extracted we create an implementation of ModuleInstaller class which has a method called start() . This method will automatically download the module (if not already installed) & run the required activity associated with that feature.

The class is flexible & most of the implementation is omitted for brevity but it has other responsibilities like showing a dialog, being lifecycle aware to avoid any memory leaks, etc.

After shipping the feature, we observe a 55% reduction in our app i.e going from 19 MB to 11 MB. This is a huge achievement. We currently haven’t captured any metrics which would determine how this affected overall installs, but we assume a reduction in app size would always benefit users especially if they have low bandwidth & are using low-end devices.

Challenges we faced with Dynamic Delivery

Even though implementing dynamic delivery gave us a lot of promising features, there were some issues we faced during & after development.

If you’ve enabled R8 then make sure to exclude resources which are referenced in the dynamic module or face a runtime crash:

R8 allows you to optimize your code and removes unused resources when building for release build. The problem is supposed you’ve got a resource in the source module (:app) & you are referencing the resource in a dynamic module (total reference count is 1), in this situation a release build with R8 enabled removes that referenced resource because the way dynamic delivery works is it creates separate apks for each dynamic module (& that is how it delivers the feature when requested). These apks do not know about each other so R8 will see that the reference count is 0 & will remove the resource. When you launch the activity or any feature from that module referencing that non-existed resource you’ll get android.content.res.Resources$NotFoundException .

To get away with this issue either move the reference resource into the dynamic module or maintain a custom resource keep file.

Resource shrinker will crash the app if you tried to test a release apk:

One of the problems we faced after implementing dynamic delivery is that if you enabled resource shrinker with R8 in the base module & build a release apk to test, the app will just crash at startup. We faced this issue when benchmarking startup time.

We tried to mitigate it by enabling the new experimental resource shrinker from gradle.properties file android.experimental.enableNewResourceShrinker=true mentioned on this issue-tracker but still failed & now we cannot benchmark with resource shrinking enabled. The solution we opt for is whenever we need to test a release build we just disable shrinkResources .

Conclusion

Implementing dynamic delivery for some of our major app size contributing features reduced install times.

One of the things I liked about dynamic delivery is that the delivery rate to which it delivers modules to the user’s device is extremely fast like 1–2 seconds so it would hardly affect the user’s experience. Nevertheless, you can always show a dialog with a beautiful Lottie animation to keep the user’s attention.

--

--