Tuesday, February 21, 2023

Handling multiple build variants (flavors) resources with Gradle script (Android)

 

Requirements

Well, we've been asked to create a project which can generate different apps with different package names, and each of them has a different icon/png set and string values.

We want the user can handle all the APK generating and signing jobs by himself ( with minimum knowledge in Android programming).

I've seen some good example in StackOverflow but most of them doesn't really meet what I want so I'm creating one for myself.

Solution

This gradle script demonstrates how to handle different resources for each flavor:

  • 3 apps defined in the project with 3 different package name (com.demo.appacom.demo.appb, com.demo.appc) ;
  • 2 sets of image *.png files and strings.xml resources (pack1pack2) being applied to different app. pack1pack2 are under the root directory of Android project.

In app/src/main/AndroidManifest.xml we have:

${appIcon} refers to manifestPlaceholders for each flavor in build.gradle:

manifestPlaceholders = [
    appIcon: "@drawable/app_icon_appa"
]

Since app icon files are pretty small so we're not going to hassle with the copy to replace job, we simply put all app icons in res/drawable though there should only be one for each flavor.

In java sources, we use R.string.app_name and R.string.WebServer to refer to resValue for each flavor in gradle so we don't have to define values in strings.xml thus the user can maintain it thru build.gradle easily :

resValue "string", "app_name", "APP_A"
resValue "string", "WebServer", "http://192.168.0.1/appA"

Resource copy

In afterEvaulate event, we use regular expression to scrap current running flavor and buildType so we can inject resource copy tasks into generate[Flavor][Build]Resources, e.g.: generateAppaDebugResources or generateAppbReleaseResources depends on current building flavor and build.

The code below demonstrates how to read current flavor (appa/appb/appc) and build type (debug/release):

def regex = "([^A-Z][a-z]+)([A-Z][^A-Z]+)([A-Z][^A-Z]+).*" 
def resTask = (gradle.startParameter.taskNames[0] =~ /$regex/) 
if (resTask.count==1) { 
    def flavor = resTask[0][2] 
    def type = resTask[0][3]
...

Then we inject tasks:

tasks."generate${flavor}${type}Resources".dependsOn copyPack1Png
tasks."generate${flavor}${type}Resources".dependsOn copyPack1String

Result: IDE build

Now the user can select any flavor from "Build Variants" panel in Android Studio (3.0) - all appa, appb, appc have their Debug/Release options available. And the APK will be signed if the user choose to generate Release APK.

Result: Command line build

The user can simply issue command line to create an APK:

  • gradlew assembleAppaDebug
  • gradlew assembleAppaRelease
  • ... etc

Bonus

The user can create new apps easily (of course he can only change image and string resources, not the program logic). Simply create another resource pack in root directory, then copy/paste those related blocks in build.gradle and it's all done.

Full build.gradle

apply plugin: 'com.android.application'

android {
    flavorDimensions "versionCode"
    signingConfigs {
        release {
            storeFile file("keystore")
            storePassword "password"
            keyAlias "keyname"
            keyPassword "password"
        }
    }
    compileSdkVersion 24
    buildToolsVersion '26.0.2'
    defaultConfig {
        minSdkVersion 21
        targetSdkVersion 24
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            proguardFiles.add(file('proguard-gvr.txt'))
            signingConfig signingConfigs.release
        }
    }

    task ("copyPack1Png", type: Copy) {
        println 'copyPack1Png'
        from('../pack1')
        into('src/main/res/mipmap-hdpi')
        include('*.png')
    }

    task ("copyPack1String", type: Copy) {
        println 'copyPack1String'
        from('../pack1')
        into('src/main/res/values')
        include('strings.xml')
    }
    task ("copyPack2Png", type: Copy) {
        println 'copyPack2Png'
        from('../pack2')
        into('src/main/res/mipmap-hdpi')
        include('*.png')
    }

    task ("copyPack2String", type: Copy) {
        println 'copyPack2String'
        from('../pack2')
        into('src/main/res/values')
        include('strings.xml')
    }

    productFlavors {
        appa {
            applicationId "com.demo.appa"
            versionCode 1
            versionName "2017.12.10"
            resValue "string", "app_name", "APP_A"
            resValue "string", "WebServer", "http://192.168.0.1/appA"
            manifestPlaceholders = [
                    appIcon: "@drawable/app_logo_appa"
            ]
        }
        appb {
            applicationId "com.demo.appb"
            versionCode 1
            versionName "2017.12.10"
            resValue "string", "app_name", "APP_B"
            resValue "string", "WebServer", "http://192.168.0.1/appB"
            manifestPlaceholders = [
                    appIcon: "@drawable/app_logo_appb"
            ]
        }
        appc {
            applicationId "com.demo.appc"
            versionCode 1
            versionName "2017.12.10"
            resValue "string", "app_name", "APP_C"
            resValue "string", "WebServer", "http://192.168.0.1/appC"
            manifestPlaceholders = [
                    appIcon: "@drawable/app_logo_appc"
            ]
        }
    }
}

dependencies {
    // ....
    // omitted
}

afterEvaluate {
    def regex = "([^A-Z][a-z]+)([A-Z][^A-Z]+)([A-Z][^A-Z]+).*"
    def resTask = (gradle.startParameter.taskNames[0] =~ /$regex/)

    if (resTask.count==1) {
        def flavor = resTask[0][2]
        def type = resTask[0][3]

        println resTask[0][2]
        println resTask[0][3]
        switch (flavor.toString().toLowerCase()) {
            case "appa":
                println "Applying package resources..."
                android.productFlavors.appa {
                    tasks."generate${flavor}${type}Resources".dependsOn copyPack1Png
                    tasks."generate${flavor}${type}Resources".dependsOn copyPack1String
                }
                break
            case "appb":
                println "Applying package resources..."
                android.productFlavors.appb {
                    tasks."generate${flavor}${type}Resources".dependsOn copyPack2Png
                    tasks."generate${flavor}${type}Resources".dependsOn copyPack2String
                }
                break
            case "appc":
                println "Applying package resources..."
                android.productFlavors.appc {
                    tasks."generate${flavor}${type}Resources".dependsOn copyPack2Png
                    tasks."generate${flavor}${type}Resources".dependsOn copyPack2String
                }
                break
        }
    }
}

No comments:

Post a Comment