Gradle

Budowanie

  |   8 min czytania

Wstęp

System budowania aplikacji Android kompiluje zasoby, kod źródłowy oraz pakiety do pliku w formacie APK (Android Application Package), który może być testowany, podpisany i dystrybuowany. W tym celu Android Studio używa zaawansowanego zestawu narzędzi Gradle wraz z dedykowanym Android Gradle plugin pozwalających na zarządzanie i automatyzację tego procesu dzięki czemu każda konfiguracja może definiować własne zasady budowania projektu. Gradle oraz plugin działają niezależnie od Android Studio co pozwala na kompilacje bez udziału środowiska programistycznego, np. przez wiersz poleceń.

Proces

Budowanie aplikacji angażuje wiele narzędzi i uruchamia różne procesy umożliwiające konwersje projektu do APK. Typowy proces przebiega następująco. Na początku kompilator konwertuje kod źródłowy do plików DEX (Dalvik Executable) zawierających kod bajtowy, a pozostałe pliki i zależności do skompilowanych zasobów. Następnie APK Packager łączy i optymalizuje pliki DEX i skompilowane zasoby do jednego pliku APK podpisując go kluczem debug lub release z keystore. Powstały plik APK jest gotowy do instalacji, debugowania czy testowania.

Konfiguracja

Dokonując konfiguracji budowania projektu można wyróżnić kilka aspektów wpływających na wyjściowy rezultat. buildTypes definiuje właściwości dla wydań (np. zaciemnienie release), productFlavors reprezentuje różne wersje aplikacji (np. płatna, darmowa, demo). buildVariants jest konkretnym wariantem budowania wynikającym z połączenia wybranych buildTypes i productFlavors, które mogą używać współdzielonych jak i prywatnych zasobów. Wartości wpisów w AndroidManifest mogą się różnić w zależności od wariantu budowania (np. inna nazwa czy różne minSdk). System budowania zarządza także wpisami lokalnych i zdalnych zależności dependencies dzięki czemu nie ma potrzeby ręcznego szukania, pobierania i kopiowania pakietów zależności do projektu. Ponadto umożliwia ustawienie podpisu autentykacyjnego i zasad bezpieczeństwa ProGuard oraz wspiera budowanie wielu APK.

Pliki

Informacje nt konfiguracji znajdują sie w kilku plikach projektu należących do danego modułu. Używają one DSL (Domain Specific Language) do opisania i manipulowania logiką przy pomocy Groovy (dynamicznego języka dla JVM). Android Gradle plugin dostarcza większość potrzebnych elementów DSL w związku z czym nie jest wymagana wiedza programowania w Groovy.

settings.gradle deklaruje moduły, które powinny wziąć udział w procesie budowania projektu

include ':app', ':module1', ':module2'

build.gradle znajdujący się w głównym katalogu definuje konfigurację wspólną dla wszystkich modułów w projekcie, np. repozytoria i wersja Sdk

// Configure the repositories and dependencies needed for Gradle itself in buildscript block
buildscript {

    //Define some shared properties
    ext {
        compileSdkVersion = 28
        supportLibVersion = "28.0.0"
        kotlin_version = '1.3.31'
    }

    // Pass repositories for Gradle to search and download dependencies
    repositories {
        google()
        jcenter()
    }

    // Pass dependencies for Gradle to build project
    dependencies {
        classpath 'com.android.tools.build:gradle:3.4.1'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

// Configure the global repositories and depenendencies used by all modules
allprojects {
   repositories {
       google()
       jcenter()
   }
}

// Create some tasks if needed
task clean(type: Delete) {
    delete rootProject.buildDir
}

build.gradle znajdujący się w każdym module pozwala na specyficzną konfiguracje dla danego modułu uzupełniając lub nadpisując definicję build.gradle projektu, np. zależności, wtyczki czy konfiguracja wersji i wariantów budowanej paczki APK

//apply plugins to build and makes android block available to build options
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

// Configure Android specific build options
android {

    // Define some compile and build properties
    compileSdkVersion 28

    // Specify default settings and entries for all build variant
    defaultConfig {
        applicationId "pl.androidcode.app"
        minSdkVersion 21
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    // Configure multiple build types like apply Proguard for release and make debug debuggable
    buildTypes {
        release {
            minifyEnabled true
            debuggable false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
        debug {
            minifyEnabled false
            debuggable true
            signingConfig signingConfigs.debug
        }
    }

    // Configure multiple product flavors, override defaultConfig block settings
    // At least one flavorDimensions must be declared, they combine multiple flavors
    flavorDimensions "version"
    productFlavors {
        free {
            dimension "version"
            applicationIdSuffix ".free"
            versionNameSuffix "-free"
        }
        paid {
            dimension "version"
            applicationIdSuffix ".paid"
            versionNameSuffix "-paid"
        }
    }

    // Use filter to disable some build variant
    variantFilter { variant ->
        def flavors = variant.flavors*.name
        def types = variant.buildType*.name
        if (types.contains("debug") && flavors.contains("paid")) {
            setIgnore(true)
        }
    }
}

// Provide dependencies needed only for module itself
dependencies {
    //local binaries
    implementation fileTree(dir: 'libs', include: ['*.jar']) 

    //remote binaries
    implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version' 
    implementation 'androidx.appcompat:appcompat:1.0.2'
    implementation 'androidx.core:core-ktx:1.0.2'

    //add build type or flavor prefix to use implementation only for this variant
    paidImplementation 'com.android.billingclient:billing:2.0.1'

    //unit and instrumental tests
    //notice that test and androidTest are source sets like any other build variant
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test:runner:1.2.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}

gradle.properties określa ustawienia Gradle dla całego projektu, np. maksymalna wielkość stosu deamona czy użycie artefaktów AndroidX

org.gradle.jvmargs=-Xmx1536m
android.useAndroidX=true
android.enableJetifier=true
kotlin.code.style=official

local.properties konfiguruje lokalne właściwości środowiska dla systemu kompilacji, np. ścieżka instalacji Sdk

sdk.dir=C\:\\Android\\Sdk

Źródła

Aby budowane warianty aplikacji zadeklarowane w build.gradle rzeczywiście różniły się implementacją należy stworzyć dla nich lokalizacje odpowiadające nazwie wariantu oraz umieścić tam specyficzny kod źródłowy i zasoby. Dla zdefiniowanych powyżej wariantów ich zbiory źródeł mogłyby znajdować się w: src/debug, src/release, src/debugFree, src/debugPaid, src/releaseFree, src/releasePaid. Zawartość src/main jest traktowana jako domyślna i współdzielona przez wszystkie warianty kompilacji natomiast źródła konkretnych wariantów nadpisują implementację bazową zgodnie z zasadą priorytetów (build variant > build type > build flavor > main > library).

Optymalizacja

Tworząc konfiguracje budowania aplikacji należy rozważyć optymalizację czasu procesu kompilacji oraz rozmiaru pliku wyjściowego. Budowanie wielu APK dedykowanych pod konkretne architektury czy gęstości ekranów pozwala zmniejszyć rozmiar poprzez załączenie tylko wymaganych zasobów. Jednakże taki proces znacząco wydłuża czas całkowitej kompilacji w związku z czym warto wyłączyć niepotrzebne wersje oraz zawężyć wariant deweloperski. Ponadto zastosowanie zasad ProGuard także umożliwia redukcje rozmiaru przy jednoczesnym spowolnieniu kompilacji. Użycie statycznych zależności, trybu offline, cache czy Instant Run przyśpiesza budowanie projektu. W przypadku wersji produkcyjnej przeważnie dąży się przede wszystkim do optymalizacji rozmiaru natomiast w wersji deweloperskiej do optymalizacji czasu kompilacji.

android {

    //...

    flavorDimensions "stage", "version"
    productFlavors {
        //...
        
        //add developer and production flavor with new dimension
        dev {
            //...
            dimension "stage"
            resConfigs "pl", "xxhdpi" //attach only pl resources
        }
        prod {
            //...
            dimension "stage"
        }
    }

    // Configure different APK builds that each contains only needed code and resources for density and abi
    // Notice that every build must have unique version code for store
    splits {
        density {
            enable true
            exclude "ldpi"
        }
        abi {
            enable gradle.startParameter.taskNames.any { it.contains("release") }
            universalApk false
        }
    }
}