Automating Data Conversion in a Multi-Flavored Application

In our multi-flavored application, ensuring correct embedded data files for each build was challenging. We automated the data conversion script to run before building the app, preventing errors caused by manual oversight.

Automating Data Conversion in a Multi-Flavored Application

In my application, we need to build multiple versions from a single codebase. Each version, or flavor, requires different embedded data files. To manage this, we created a conversion script that must be run each time we switch to a different flavor. However, developers sometimes forget to run this script, leading to incorrect data in the application.

To solve this, I wanted to automate the conversion script to run automatically before building the application. Here’s how I achieved this.

Initial Attempt

After some investigation, I found that we could create a task in build.gradle.kts and make the assemble tasks depend on this conversion task. Here's the initial code I used:

fun createConvertTask(appFlavor: String) {
    val taskName = getAppFlavorConvertTaskName(appFlavor)
    tasks.register(taskName, org.gradle.api.tasks.Exec::class) {
        // Set the description and group for better organization and clarity in Gradle tasks
        description = "Converts data for $appFlavor flavor"
        group = "Conversion"

        // Set the working directory to the parent directory
        workingDir = file("$projectDir/..")

        // Set the command line to run the script with the app flavor as argument
        commandLine("sh", "./convert.sh", appFlavor)
    }
}

Since tasks can be added dynamically, we used a callback on tasks.whenTaskAdded to ensure our conversion task runs before the assemble tasks:

    if (name.startsWith("assemble") && !name.endsWith("Test")) {
        // Updated regex to handle specific build types 'Debug' or 'Release'
        val regex = Regex("assemble([A-Z][^A-Z]*)(Debug|Release)")

        val matchResult = regex.find(name)

        if (matchResult != null) {
            val (appFlavor, _) = matchResult.destructured
            this.dependsOn(getAppFlavorConvertTaskName(appFlavor))
        }
    }
}

However, when attempting to build the release version of the app, we encountered the following error:

ERROR: <app_root>/build/intermediates/merged_java_res/<app_release>/base.jar: R8: com.android.tools.r8.ResourceException: com.android.tools.r8.internal.Ub: I/O exception while reading '<app_root>/build/intermediates/merged_java_res/<app_release>/base.jar': <app_root>/build/intermediates/merged_java_res/<app_release>/base.jar

Solution

To resolve this issue, I moved the configuration to the afterEvaluate block. This ensures that all tasks are fully configured before we add dependencies. Here’s the final solution:

afterEvaluate {
    tasks.withType<DefaultTask>().configureEach {
        val regex = Regex("assemble([A-Z][^A-Z]*)(Debug|Release)")

        val matchResult = regex.find(name)

        if (matchResult != null) {
            val (appFlavor, _) = matchResult.destructured
            this.dependsOn(getAppFlavorConvertTaskName(appFlavor))
        }
    }
}

With this change, the APK builds successfully, and the conversion script runs automatically, ensuring the correct data files are embedded for each flavor.