Categories:
Search articles

Search for articles

Sorry, but we couldn't find any matches...

But perhaps we can interest you in one of our more popular articles?

Android app performance: Gradle and YAML (part 1)

Nov 17, 2020

How to build native Android apps on Codemagic

How to build native Android apps on Codemagic

Use codemagic.yaml file to keep your build configurations more organized and manage all your workflows
Read more
Codemagic builds and tests your app after every commit, notifies selected team members and releases to the end user. Automatically. Get started

Written by Sneh Pandya

With millions of Android developers who develop over a million apps every year, it has become crucial for these mobile app developers to make the apps feature-rich, small in size, and robust all at the same time. But there are endless variations and combinations between devices, operating system versions, compatibility features, and more.

How do you make sure your apps are built for your diverse audience, are fast, and give you the power to cater only the resources that are needed to each specific user, all while keeping the app size as small as possible? The answer: Gradle. Let’s find out how!

What is Gradle, actually?

Gradle is a flexible general-purpose build automation tool made for building almost any type of programming-language-based software development. Gradle runs multiple tasks based on the development or deployment required. The tasks compile the code for packaging, testing, deployment, and publishing.

The Gradle build system is based on Groovy, which is an object-oriented programming language. Gradle avoids unnecessary work by only running the tasks that need to run based on changes in their inputs or outputs. A build cache can also be used to enable the reuse of task outputs from previous runs or even from a different machine (with a shared build cache). Constant improvements and various optimizations make Gradle a high-performance and versatile tool.

API levels and backward compatibility

Choosing the right SDK version is very crucial for Android applications, as certain APIs are allowed and restricted based on the supported SDK versions. Hence, choosing the right SDK version also depends on the specific use case that you want to achieve.

The Android Studio team has included this smart guide, which outlines all the necessary API-specific details for each version. This will help you pick the right minimum API version to support for your end users. The API level distribution chart is available in Android Studio → Start a new Android Studio Project → Select a Project Template → Configure Your Project → click on Help me choose below Minimum SDK.

Ideally, developers keep the minimum API level at 21, supporting devices running Android OS 5.0 (Lollipop), which supports approximately 94% of Android smartphones in the market today. You should also consider when choosing the lower API versions that they also have their trade-offs for compatibility. For the same reason, when developing for newer versions of Android, a best practice is to use backward-compatible APIs and libraries. The AndroidX packaging, which comes under the newly announced Android Jetpack libraries, provides feature parity, new libraries, and full backward compatibility across Android releases.

We will keep the minSdkVersion 16 in our project to showcase the best practices and improvements applied and discussed throughout this article.

Data binding vs. view binding: What to choose?

The Data Binding Library for Android was introduced back in 2015, and since then, there have been multiple successive improvements. View binding is a successor to data binding that was released in 2019. Both of the libraries do their work pretty neatly. But which one should you choose? Let’s find out!

The Data Binding Library allows you to bind code to the views and vice versa to pass the data between them, whereas view binding binds only views to the code. In simpler terms, you cannot use view binding to bind layouts with data in XML. This means you cannot use binding expressions in your layout files or even two-way binding with the View Binding Library.

The advantage of the Data Binding Library is that it includes all the features from the View Binding Library and more, but it comes with the cost of longer build times and generated multiple classes. Also, the View Binding Library removes the declarations of binding classes in your code and the <data> tag in view files. You should choose view binding if you only want to reference your views in your code. Avoid NullPointerException and findViewById. If you want to receive updates in your code when the data changes in the views or in other cases, like when you want to bind adapters, for example, then you should choose data binding.

Data binding and view binding are enabled in the app-level build.gradle file. However, they cannot be specified together at once. You can either declare DataBinding or ViewBinding in your build.gradle file.

Data binding or view binding can be enabled by specifying the blocks below:

// DataBinding
android {
    ...
    buildFeatures {
        dataBinding {
            enabled true
        }
    }
    ....
}

// ViewBinding
android {
    ...
    buildFeatures {
        viewBinding {
            enabled true
        }
    }
    ....
}

minifyEnabled, shrinkResources, ProGuard, and R8: What’s the difference?

minifyEnabled enables code shrinking, obfuscation, and optimization for only the project’s release build type. Code shrinking with R8 is enabled by default when you set the minifyEnabled property to true.

For Android Studio 3.4 or Android Gradle plugin 3.4.0 and higher, R8 is the default compiler that converts the project’s Java bytecode into the DEX format that runs on the Android platform.

When the minifyEnabled property is set to true, R8 combines rules from all the available sources, including ProGuard, library dependencies, custom configuration files, AAPT2, and the Android Gradle plugin.

shrinkResources enables resource shrinking, which is performed by the Android Gradle plugin. In the newer versions of Android Studio and the Android Gradle plugin, crunchPngs can also be set to true to shrink the PNG image resources for your Android project.

proguard is used to apply the ProGuard rules files that are packaged with the Android Gradle plugin. When a new project or module is created using Android Studio, the IDE creates a proguard-rules.pro file for each module directory to include your own rules. Also, additional rules can be included from other files by adding them to the proguardFiles property in the module’s build.gradle file.

IMPORTANT: It is highly recommended that you thoroughly test your app before publishing since ProGuard tends to remove code that might be used by the application. To specify and include the classes in build generation, please refer to the official ProGuard documentation to learn how to use -keep rules.

After R8 obfuscates your code, understanding a stack trace is extremely difficult because the class and method names and even the line numbers are changed. R8 creates a mapping.txt file each time it runs, which contains the obfuscated class, method, and field names mapped to the original names. The file is stored in the <module- name>/build/outputs/mapping/<build-type>/ directory.

R8 generates the mapping.txt file every time the project is built, so each time a new release is published, you also need to publish the latest version of the mapping.txt file. That way, you’ll be able to debug a problem if an obfuscated stack trace of the crash or error from an older version of the app is submitted.

The app shrinking can be enabled by specifying the block below:

android {
    
    buildTypes {
        release {
            crunchPngs true
            minifyEnabled true
            shrinkResources true
            proguardFile getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.release
        }
    }
    
}

Learn target-specific APK generation

Usually, while publishing the app for the end users, we tend to build the APK via a standard method, but even after applying proguard, minification, and shrinking, the app that is generated has a lot of bloatware resources that are not useful for the targeted devices specifically.

These resources are commonly screen-density-specific layouts, device-architecture-specific modules, and more. Not every end user needs them. It is a best practice to split them into various target-specific or device-specific APKs and publish them.

The abi filter splits and generates APKs specific to the device architectures. Per-architecture-based APKs are smaller in size since the native code included for the device architecture is compiled in the specific output APK only.

android {
  ...
    splits {

      // Configures multiple APKs based on ABI.
        abi {

            // Enables building multiple APKs per ABI.
            enable true

            // By default, all ABIs are included, so use reset() and include to specify that we only want
            // Resets the list of ABIs that Gradle should create APKs for to none.
            reset()

            // Specifies a list of ABIs that Gradle should create APKs for.
            include "x86", "x86_64"

            // Specifies that we do not want to also generate a universal APK that includes all ABIs.
            universalApk false
        }
    }
    ...
}

The density filter splits and generates APKs based on specific device densities and screen sizes. In the density block, provide a list of desired screen densities and compatible screen sizes that you want to include in the APK generation.

android {
    ...
    splits {

        // Configures multiple APKs based on screen density.
        density {

            // Configures multiple APKs based on screen density.
            enable true

            // Specifies a list of screen densities Gradle should not create multiple APKs for.
            exclude "xxxhdpi"

            // Specifies a list of compatible screen size settings for the manifest.
            compatibleScreens 'small', 'normal', 'large', 'xlarge'
        }
    }
    ...
}

Make your machine build faster!

Android uses the Dalvik Virtual Machine (DVM) instead of the Java Virtual Machine to generate the bytecode. Hence, the DVM generates output files known as DEX: Dalvik EXecutables. Essentially, Android apps are compiled into .dex (Dalvik EXecutable) files, which are in turn zipped into a single APK file on the device.

Gradle gives you the power to boost the APK generation process, even at the DEX level! Gradle allows you to declare dexOptions where you can specify preDexLibraries to choose whether you want to use the pre-compiled or previously generated DEX output libraries for unchanged code. Moreover, it also allows you to specify the maximum process count and the size of memory allocated to the virtual machine for code compilation.

android {
    
    dexOptions {
        preDexLibraries true
        maxProcessCount 8         //default is 4
        javaMaxHeapSize "4g"      //ideally half the size of your memory
    }
    
}

Similarly, there are a few other declarations that would make your Gradle execute even faster! Gradle runs in a separate process, enabling the Gradle daemon to make the startup and execution triggers faster for your Android project. Similar to the previously described preDexLibraries, Gradle also provides its own caching mechanism, which can be enabled. To invoke tasks based on the project changes and to execute only the tasks again when there are project changes, you can enable the configure-on-demand feature.

Most importantly, in a lot of Android projects, there is more than one module, apart from just the app module. These modules add a lot of weight to the project and execution of tasks since Gradle has to compile each of them individually. You can specify parallel execution to enable multi-module mode for Gradle, which will run tasks in parallel for each module and result in the project being built faster.

Make sure to specify these arguments in the gradle.properties file:

org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configureondemand=true

Harness the power of YAML with Codemagic

YAML is a human-friendly data representation format built for simplicity and readability. Codemagic has built-in support for YAML, which allows you to specify a lot of details, parameters, and custom configuration for your project!

Codemagic supports environment variables via YAML declarations before, during, and after the build process. You can trigger a lot of common procedures or actions by specifying environment variables using YAML for Codemagic’s environment. Going further, you can specify custom variables specific to your project, like build numbers, temporary variables, and more.

With the help of YAML, you can also encrypt sensitive data for your build process or make use of webhooks to trigger builds automatically and run tests. You can set up code signing and automated publishing and forget about the hassle of manually publishing your apps for your end users.

Finally, if you are aiming to achieve seamless environment compatibility, Codemagic CLI tools will help you replicate this on your local machines!

YAML best practices:

  1. Only use WHITESPACE characters for indentation and not TAB while writing a YAML file. Most decent editors handle this natively and can be configured even further.

  2. It is recommended to use a monospaced font to view and edit YAML so that it’s easy to spot indentation errors.

  3. Save your YAML files in the UTF-8 encoding. This is handled automatically by most of the popular editors, like Visual Studio Code.

Final words

Our final app-level build.gradle file looks like this:

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 30
    defaultConfig {
        applicationId "com.androidapp.myapp"
        minSdkVersion 16
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"
        multiDexEnabled true
    }

    buildFeatures {
        viewBinding {
            enabled true
        }
    }

    splits {
        density {
            enable true
            exclude "small", "xxxhdpi"
            compatibleScreens 'normal', 'large', 'xlarge'
        }
        abi {
            enable true
            reset()
            include "x86", "x86_64"
            universalApk false
        }
    }

    sourceSets {
        main.java.srcDirs += 'src/main/kotlin'
    }

    dexOptions {
        preDexLibraries true
        maxProcessCount 8
        javaMaxHeapSize "4g"
    }

    compileOptions {
        sourceCompatibility = 1.8
        targetCompatibility = 1.8
    }

    signingConfigs {
        release {
            storeFile file("../myappkeystore.jks")
            storePassword "myapppassword"
            keyPassword "myappkeypassword"
            keyAlias "myappalias"
        }
    }

    def proguard_list = [
            "$project.rootDir/buildsettings/proguard/proguard-constraintlayout.pro",
            "$project.rootDir/buildsettings/proguard/proguard-google-play-services.pro",
            "$project.rootDir/buildsettings/proguard/proguard-project.pro"
    ]

    buildTypes {
        release {
            debuggable false
            crunchPngs true
            minifyEnabled true
            shrinkResources true
            proguard_list.each {
                pro_guard -> proguardFile pro_guard
            }
            signingConfig signingConfigs.release
        }
        debug {
            debuggable true
            crunchPngs false
            minifyEnabled false
            shrinkResources false
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation "androidx.core:core-ktx:1.3.2"
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.10"

    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.2'
    implementation 'androidx.multidex:multidex:2.0.1'
    implementation 'com.google.android.material:material:1.2.1'
}

apply plugin: 'com.google.gms.google-services'

Gradle and YAML are powerful tools if used wisely. Using them the right way will help you build apps in a faster, smarter way—saving a lot of effort and time. With every new release, the Gradle build system is improving and giving you new opportunities to take your app development and publishing to the next level.


Sneh is a Senior Product Manager based in Baroda. He is a community organizer at Google Developers Group and co-host of NinjaTalks podcast. His passion for building meaningful products inspires him to write blogs, speak at conferences and mentor different talents. You can reach out to him over Twitter (@SnehPandya18) or via email (sneh.pandya1@gmail.com).

How did you like this article?

Oops, your feedback wasn't sent

Latest articles

Show more posts