Reduce size of Android app

3-APR-2022, last updated 3-APR-2022

When you use Android Studio to build your app, you can choose between the classic APK (.apk) and Android App Bundle (.aab). The size of it directly affects how much data the user has to download when they install or update your app.

I only test app size improvements with the APK. Most improvements reduce the Bundle size as well, but some may be APK specific.

Just to give you an indication of what all this can achieve: my APK is currently only 1.03 Megabytes.

Table of Contents

Bundle is smaller than APK

Both an initial download as well as an update of an Andriod App Bundle is smaller than downloading an APK. So from a size point of view, a bundle is better than an APK. So if you distribute your app through the Google Play Store, then it's best to update a bundle there. The size difference can be really big.

Note that when a user changes their setting, Android may have to download another bundle. With an APK, you will not have that problem.

↑ Back to menu

Grade shrink & Proguard & R8

As explained in more detail elsewhere, the gradle build system allows you to reduce the size, simply by minimizing source code, shrinking resouces and Proguard tree shaking.

This is what my build.gradle of the app looks like, related to this topic.

// file: build.grade (:app)

android {
    // other stuff
  
    buildTypes {
        release {
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

After you have added this, and you "Generate Signed Bundle / APK...", it will take longer to build the app, but will result is smaller.

Another size optimization is to use the "full" R8 optimization engine..
When you enable this, make sure you test your APK afterwards, because it might break your app. My app had no issues afte enbaling this, but you need to make sure.

Also, it can cause the build to fail in some situations. For example, when I updated a library (from 'com.google.firebase:firebase-messaging:23.0.0' to 'com.google.firebase:firebase-messaging:23.0.1'), it caused the build system to fail. In that particular case, I decided to skip that library (bloated) bug fix.

You can enable the "full" R8 engine in the gradle.properties file

# file: gradle.properties

android.enableR8.fullMode=true

↑ Back to menu

Target languages

This optimization is specific for the APK.

You can specify that you don't need certain resources in your APK, like languages for which you don't have any specific translations or screen resolutions that you are not interested in. You do this by specifying the resources that you do want in 'resConfigs' in the build grade file of the app.

If you specify a language, any other non-listed language will be omitted from the APK. If you specify a screen resolution, then all the screen resolutions you didn't specify will be omitted. This will reduce the app size considerably.

My app only supports Dutch and English. In addition, I have chosen to only bundle the 480dpi screen size images (xxhdpi).

// file: build.grade (:app)

android {
    // other stuff
  
    defaultConfig {
        // other stuff
        
        resConfigs 'en', 'nl', 'xxhdpi'
    }
}

↑ Back to menu

Disable logging

All through my code, I have Log.d, Log.i and Log.e statements. But I don't need those in the final product.

Excluding the Log.* statements from the code will reduce the app size.

You can keep them in your source code, and instruct Proguard to exclude it from the build.
Assuming you have configured a 'proguard-rules.pro' file (see section Proguard), you can do that as shown below.
# file: proguard-rules.pro

# disable logging
-assumenosideeffects class android.util.Log {*;}

If you don't want to remove all Log statement, but only for example the Log.d and Log.v statement, then that's possible too, with a more elaborate syntax.

↑ Back to menu

Remove unused libraries

Obviously, you should remove libraries that are not needed. Only the 'implementation' libraries listed in the build.gradle file are included in the APK/bundle. When it comes to app size, you can ignore the 'androidTestImplementation' and 'testImplementation' libraries.

If a library is only needed in the tests, make sure it is included as 'androidTestImplementation' or 'testImplementation', to prevent it from ending up in your final product.

After you have removed a library, make sure to test your build.

In my case, I could (apparently) safely remove a legacy-support library

// file: build.grade (:app)

dependencies {
    // other stuff
    
    // Apparently, my app doesn't need the legacy-support-v4 library anymore
    //implementation 'androidx.legacy:legacy-support-v4:1.0.0'
    implementation 'androidx.appcompat:appcompat:1.4.1'
}

↑ Back to menu

Replace bitmap images with vector graphics

Where possible, replace each Image Asset with a Vector Asset.

Note that if you add the extension ".zip" to your APK, then you can inspect the contents in a ZIP file viewer. This way, you can see what PNG images are bundled.

If your app targets SDK version 22 or lower, then you could consider creating a custom view that draws your vector asset.

If you can raise your minSdkVersion to 23, then that will remove bitmap renderings of vector assets. Note that raising the minSdkVersion level means that devices that run Android 5 or older can no longer run your app.

If you do need bitmap images, then consider using a service like https://tinypng.com/ to reduce the image size.

↑ Back to menu

Optimize data lists

Your app may contain an list of objects. For example, my app has a list of Stations. Each Station is an object, with properties like Name, Station Code, Official Station Code, Latitude, Longitude, etc.

The space efficient way to deal with this, is have each object as a single string. Then with code, you create the list you need, "unpack" each of these strings and instantiate the objects when the list is needed.

It may look very cryptic, but here's what that this looks like in practice. Note that with a script the latitude and longitudes have each been transformed to two string characters that you see below (position 2 and 4). The Station constructor reconstructs the original values.

// app specific file: Stations.java

public class Stations {
    // other stuff
    
    private final List<Station> aStations;
    private final HashMap<String, Station> hmStations;

    // Constructors
    private Stations() {
        String[] stations = new String[]{
            // other stuff
            
            // Single string from each Station
            // The compiler will reduce each line to a single string
            // The concatenation here is used for some basic form of readability and implies individual attributes
            // Some attributes are fixed length, others are recognized by data domain (e.g. numbers versus letters)
            "Ut"+"P@"+"=T"+"UT"+1+"Utrecht Centraal",
            "Ae"+"S)"+";T"+"ASD"+1+"Amsterdam Centraal",
        }
        
        // other stuff
        aStations = new ArrayList<>();
        hmStations = new HashMap<>();
    
        for(String p : stations) {
            // this Station constructor will unpack "p" and create a regular Station object
            s = new Station(p);
            aStations.add(s);
            abbreviation = s.getAbbreviation();
            hmStations.put(abbreviation, s);
        }
    }
    
}

When you consider this, please note, that this unpacking (and the preparation of packing) increases complexity, which means it increases the chance of bugs. Also, you can expect a bigger memory footprint, at least temporarily.

↑ Back to menu

Enum to @IntDef

If your app has enumerations, and you use the 'enum' object for them, then you can consider changing them to integers, annotated as 'IntDef'. Android Studio will understand this, so in your code you can keep using names.

// any .java file

public class Coordinaat {

    // 1. Define your enumerations with int values
    public static final int ONBEKEND = 0;
    public static final int CENTRUM = 1;
    public static final int NOORD = 2;
    public static final int OOST = 3;
    public static final int ZUID = 4;
    public static final int WEST = 5;
    
    // 2. Declare your type and the integers it includes. In this case, the type is 'Richting'
    @IntDef({ONBEKEND, CENTRUM, NOORD, OOST, ZUID, WEST})
    @Retention(RetentionPolicy.SOURCE)
    public @interface Richting{};
    
}

// any .java file

    // Declare 'enum' variable as int with annotation of the type in step [2]
    @Coordinaat.Richting
    private int  vanuit_richting = ONBEKEND;

    // Getter
    @Coordinaat.Richting
    public int getVanuit_richting() {
        return vanuit_richting;
    }
    
    // Setter
    public void setVanuit_richting(@NonNull @Coordinaat.Richting int vanuit_richting) {
        this.vanuit_richting = vanuit_richting;
    }
    
    // Use
    if(vanuit_richting == Coordinaat.CENTRUM) {
      
    }


The @Retention(RetentionPolicy.SOURCE) will make sure that in the APK/Bundle only the actual int values are used, which typically makes the app slightly smaller. Note that the gains are small. Also, debugging may sometimes become harder, because the debugger will simply show integers.

↑ Back to menu

Mail your comments to gertjans@xs4all.nl.