DEV Community

HarmonyOS
HarmonyOS

Posted on

Best Practice Design for Handling Source and Asset Changes Across Build Variants

Read the original article:Best Practice Design for Handling Source and Asset Changes Across Build Variants

Requirement Description

In multi‑variant application development (e.g., free vs. paid, debug vs. release), it's essential to manage both source code and resources/assets (images, strings, layouts) across variants. The goal is to:

  • Create a flexible build system where shared code and variant‑specific overrides (assets or code) can coexist with minimal duplication.
  • For resources, use directory structures that allow automatic resource picking at build time without runtime branching.
  • For source code, use conditional compilation or flagging to ensure variant‑specific logic and behavior while maintaining a clean codebase.

Background Knowledge

Implementation Steps

1. Define Modules & Variants

  • Split the project into modules (e.g., commonLib, entry) and define build targets for each variant in build‑profile.json5.

  • In the root build‑profile.json5, define products and their corresponding build configurations (e.g., free/paid, debug/release). Each product points to different targets.

  • Example:

  "targets": [
          {
            "name": "default",
            "applyToProducts": [ "default" ]
          },
          {
            "name": "free",
            "applyToProducts": [ "free" ]
          },
          {
            "name": "paid",
            "applyToProducts": [ "paid" ]
          }
        ]
Enter fullscreen mode Exit fullscreen mode

2. Organize Resource Folders for Variants

  • Use a resource folder structure that supports variant overrides, such as:
  AppScope
  ├───common
  │    └───media
  │           └─── image.png
  ├───freeResources
  │    └───media
  │           └─── image.png
  └───paidResources
       └───media
            └─── image.png
Enter fullscreen mode Exit fullscreen mode
  • In the build‑profile.json5 for each module, map the appropriate resource directories for each target. HarmonyOS supports this and resolves the correct resource automatically.

  • Example for a module:

  "products": [
        {
          "name": "free",
          "resource" : {
            "directories" : [
              "./AppScope/freeResources"
            ]
          }
        },
        {
          "name": "paid",
          "resource" : {
            "directories" : [
              "./AppScope/paidResources"
            ]
          }
        }
  ]
Enter fullscreen mode Exit fullscreen mode
  • When building the "paid" variant, the build system will pick image.png from paid/, and if not available, it will fallback to common/.

3. Handle Source Code Management (Conditional Compilation)

  • For source code, you can conditionally compile or execute variant-specific logic using flags set in build‑profile.json5 (e.g., IS_PREMIUM). You can then use these flags in your code to determine which logic or functionality to enable based on the variant.

  • Use these flags in source code to adjust logic:

  import BuildProfile from "BuildProfile"

  @Entry
  @Component
  struct Index {
    public target = BuildProfile.TARGET_NAME

    @Builder
    private setupPremiumFeatures() {
      Text("Premium")
    }

    @Builder
    private setupFreeFeatures() {
      Text("Free")
    }

    build() {
      Column() {
        if (this.target === 'paid') {
          this.setupPremiumFeatures();
        } else {
          this.setupFreeFeatures();
        }
      }
      .width('100%')
      .height('100%')
    }
  }
Enter fullscreen mode Exit fullscreen mode

4. Asset and Source Override Workflow

  • For shared resources, update the common resource folder.
  • For variant‑specific changes, place assets in the respective variant folders (e.g., free/, paid/).
  • For source code, conditionally compile based on buildProfileFields. Use if/else for runtime decisions if variants cannot be handled by the build system alone.
  • Example:
    • Shared assets: common/logo.png is used across all variants unless overridden by paid/logo.png.
    • Conditional code: Different API endpoints for paid and free users, determined by BuildProfile.TARGET_NAME.

5. Create BuildProfile class

Select the module for build and choose Build > Generate Build Profile ${moduleName} on the menu bar.

Code Snippet / Configuration

Project-level build‑profile.json5 :

{
  "app": {
    "signingConfigs": [],
    "products": [
      {
        "name": "default",
        "signingConfig": "default",
        "targetSdkVersion": "6.0.0(20)",
          "compatibleSdkVersion": "6.0.0(20)",
            "runtimeOS":"HarmonyOS",
        "buildOption": {
          "strictMode": {
            "caseSensitiveCheck": true,
            "useNormalizedOHMUrl": true
          }
        }
      },
      {
        "name": "free",
        "signingConfig": "default",
        "targetSdkVersion": "6.0.0(20)",
        "compatibleSdkVersion": "6.0.0(20)",
        "runtimeOS":"HarmonyOS",
        "buildOption": {
          "strictMode": {
            "caseSensitiveCheck": true,
            "useNormalizedOHMUrl": true
          }
        },
        "resource" : {
          "directories" : [
            "./AppScope/freeResources"
          ]
        }
      },
      {
        "name": "paid",
        "signingConfig": "default",
        "targetSdkVersion": "6.0.0(20)",
        "compatibleSdkVersion": "6.0.0(20)",
        "runtimeOS":"HarmonyOS",
        "buildOption": {
          "strictMode": {
            "caseSensitiveCheck": true,
            "useNormalizedOHMUrl": true
          }
        },
        "resource" : {
          "directories" : [
            "./AppScope/paidResources"
          ]
        }
      }
    ],
    "buildModeSet": [
      {
          "name": "debug",
      },
      {
        "name": "release"
      }
    ]
  },
  "modules": [

  {
      "name": "entry",
      "srcPath": "./entry",
      "targets": [
        {
          "name": "default",
          "applyToProducts": [
            "default"
          ]
        },
        {
          "name": "free",
          "applyToProducts": [
            "free"
          ]
        },
        {
          "name": "paid",
          "applyToProducts": [
            "paid"
          ]
        }
      ]
    }]
}
Enter fullscreen mode Exit fullscreen mode

Module-level entry/build‑profile.json5 (resource configuration):

{
  "apiType": "stageMode",
  "buildOption": {

    "resOptions": {
      "copyCodeResource": {
        "enable": false
      }
    }
  },
  "buildOptionSet": [
    {
      "name": "free",
      "arkOptions": {
        "buildProfileFields": {
          "IS_PREMIUM": false
        },
        "obfuscation": {
          "ruleOptions": {
            "enable": false,
            "files": [
              "./obfuscation-rules.txt"
            ]
          }
        }
      }
    },
    {
      "name": "paid",
      "arkOptions": {
        "buildProfileFields": {
          "IS_PREMIUM": true
        },
        "obfuscation": {
          "ruleOptions": {
            "enable": false,
            "files": [
              "./obfuscation-rules.txt"
            ]
          }
        }
      }
    }
  ],
  "targets": [
    {
      "name": "free"
    },
    {
      "name": "paid"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Asset Directory Structure :

AppScope
├───freeResources
│    └───media
│         └─── image.png
└───paidResources
     └───media
            └─── image.png
Enter fullscreen mode Exit fullscreen mode

ArkTS Source Code with Conditional Compilation and Resource Handling:

import BuildProfile from "BuildProfile"

@Entry
@Component
struct Index {
  public target = BuildProfile.TARGET_NAME
  public img = this.getUIContext().getHostContext()?.resourceManager.getMediaContent($r('app.media.image').id)

  @Builder
  private setupPremiumFeatures() {
    Text("Premium")
  }

  @Builder
  private setupFreeFeatures() {
    Text("Free")
  }

  build() {
    Column() {
      if (this.target === 'paid') {
        this.setupPremiumFeatures();
      } else {
        this.setupFreeFeatures();
      }
      Image($r('app.media.image'))
        .width(100)
        .height(100)
        .objectFit(ImageFit.Contain)

    }
    .width('100%')
    .height('100%')
  }
}
Enter fullscreen mode Exit fullscreen mode

Test Results

  • Select product: cke_2858.png
  • Check that no unintended resources are included, and the right source code paths are compiled.
  • Asset Validation: Ensure that variant‑specific assets (e.g., images) are correctly selected based on the variant.
  • Source Validation: Ensure that the correct logic is being executed based on flags (BuildProfile.TARG_NAME).

Limitations or Considerations

This sample supports API Version 12 Release and above.
This sample supports HarmonyOS 5.0.0 Release SDK and above.
Compilation and execution require DevEco Studio 5.0.0 Release or later

Written by Bunyamin Akcay

Top comments (0)