Better way of using a few different resources files per build flavor

Learn better way of using a few different resources files per build flavor with practical examples, diagrams, and best practices. Covers android, gradle development techniques with visual explanati...

Streamlining Resource Management with Build Flavors in Android Gradle

Hero image for Better way of using a few different resources files per build flavor

Discover how to efficiently manage different resource files for various build flavors in your Android projects using Gradle, enhancing flexibility and reducing code duplication.

Android applications often require different configurations, assets, or strings depending on the target environment, user group, or distribution channel. Gradle's build flavors provide a powerful mechanism to achieve this, allowing you to create distinct versions of your app from a single codebase. A common challenge is managing resources that vary across these flavors, such as app icons, API keys, or specific layout adjustments. This article will guide you through a robust approach to organize and utilize different resource files per build flavor, ensuring a clean and maintainable project structure.

Understanding Android Build Flavors

Build flavors are a core feature of the Android build system that allow you to define custom product variations. Each flavor can have its own set of source code, resources, and manifest entries, which are merged with the main source set during the build process. This modularity is crucial for projects that need to cater to multiple requirements without maintaining separate branches or projects.

flowchart TD
    A[Base Project] --> B{Build Flavor Selection}
    B --> C1[Flavor 1 Source Set]
    B --> C2[Flavor 2 Source Set]
    C1 --> D[Merged with Main Source Set]
    C2 --> D
    D --> E[Compiled Application (APK/AAB)]
    subgraph Source Sets
        C1
        C2
    end
    subgraph Build Process
        B
        D
        E
    end

Android Build Flavor Merging Process

Structuring Resources for Build Flavors

The key to managing flavor-specific resources is understanding Gradle's source set merging rules. For each build flavor you define, Gradle automatically looks for a corresponding source set directory. If a resource exists in both a flavor's source set and the main source set, the flavor's resource takes precedence. This hierarchical merging allows you to override or add resources as needed.

Consider a scenario where you have free and paid flavors. You might want different app icons, different string values (e.g., app name), or even different layouts for each. The recommended directory structure for this is as follows:

app/
├── src/
│   ├── main/
│   │   ├── java/
│   │   ├── res/
│   │   │   ├── drawable/
│   │   │   ├── layout/
│   │   │   ├── values/
│   │   │   └── ...
│   │   └── AndroidManifest.xml
│   ├── free/
│   │   ├── java/
│   │   ├── res/
│   │   │   ├── drawable/
│   │   │   │   └── ic_launcher.xml  // Free flavor icon
│   │   │   ├── values/
│   │   │   │   └── strings.xml      // Free flavor strings
│   │   │   └── ...
│   │   └── AndroidManifest.xml
│   └── paid/
│       ├── java/
│       ├── res/
│       │   ├── drawable/
│       │   │   └── ic_launcher.xml  // Paid flavor icon
│       │   ├── values/
│       │   │   └── strings.xml      // Paid flavor strings
│       │   └── ...
│       └── AndroidManifest.xml
└── build.gradle

Recommended directory structure for flavor-specific resources

Configuring Build Flavors in build.gradle

To enable and define your build flavors, you need to configure them within your module-level build.gradle file (typically app/build.gradle). The productFlavors block is where you declare each flavor and can customize properties like applicationIdSuffix, versionNameSuffix, and resValue for simple resource overrides.

android {
    compileSdk 34

    defaultConfig {
        applicationId "com.example.myapp"
        minSdk 21
        targetSdk 34
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    flavorDimensions "version"
    productFlavors {
        free {
            dimension "version"
            applicationIdSuffix ".free"
            versionNameSuffix "-free"
            resValue "string", "app_name", "My App Free"
            // Add flavor-specific buildConfigField for API keys or other constants
            buildConfigField "String", "API_BASE_URL", "\"https://api.free.example.com\""
        }
        paid {
            dimension "version"
            applicationIdSuffix ".paid"
            versionNameSuffix "-paid"
            resValue "string", "app_name", "My App Paid"
            buildConfigField "String", "API_BASE_URL", "\"https://api.paid.example.com\""
        }
    }
}

dependencies {
    // ...
}

Example build.gradle configuration for free and paid flavors

In the example above, we define two flavors, free and paid, under the version flavor dimension. Each flavor gets a unique applicationIdSuffix and versionNameSuffix. Crucially, we use resValue to define a flavor-specific app_name string directly in the Gradle file. This is useful for simple string overrides. For more complex resource changes (like drawables or layouts), placing the files in the flavor's res directory is the preferred method.

Accessing Flavor-Specific Resources

Once configured, accessing these flavor-specific resources in your Android application is straightforward. You simply reference them by their standard resource ID, and the Android build system automatically picks the correct resource based on the active build variant.

XML Layout

Kotlin Code

// In your Activity or Fragment import android.os.Bundle import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main)

    // Accessing string resource
    val appName = getString(R.string.app_name)
    println("App Name: $appName")

    // Accessing drawable resource (example for programmatic use)
    val launcherIcon = getDrawable(R.drawable.ic_launcher)
    // Use launcherIcon as needed

    // Accessing BuildConfig field
    val apiBaseUrl = BuildConfig.API_BASE_URL
    println("API Base URL: $apiBaseUrl")
}

}

When you build the freeDebug variant, R.string.app_name will resolve to "My App Free" and R.drawable.ic_launcher will be the icon from free/res/drawable. Similarly, for paidDebug, it will resolve to "My App Paid" and the icon from paid/res/drawable.