How to Dynamically Change Your OutSystems Capacitor Mobile App Icon in ODC (Android + iOS)
Learn how to dynamically change your OutSystems Capacitor Mobile App Icon in ODC
How to Dynamically Change Your OutSystems Capacitor Mobile App Icon in ODC (Android + iOS)

Introduction
You may have noticed that some apps change their icon during special events (holiday seasons, Black Friday, campaigns, etc.). Dynamic icons can create a more personalized experience and add a “wow factor” to your app.

Duolingo dynamic icon change
After searching the OutSystems Forge for a plugin that covered this use case for both iOS and Android and not finding one, I built my own solution: a Capacitor plugin called ***capacitor-app-icon-OS*, wrapped as an ODC mobile library named “App Icon Plugin”** (available in ODC Forge).
In this article, I’ll walk you step-by-step through the required configuration, how to use the plugin in ODC, and successfully change your app icon dynamically.
What you’ll need
- ODC + MABS 12 (Capacitor-based mobile builds)
- The App Icon Plugin mobile library
- Build Actions (JSON) to patch Android and iOS project files
- App icon resources added to your app
Android: Changing the icon dynamically
On Android, dynamic icons are typically implemented using activity-alias entries in AndroidManifest.xml. Each alias points to the same MainActivity, but with a different icon.
In a typical Capacitor Android project, the launcher intent is usually on MainActivity:
....
<activity
...
android:name=".MainActivity"
....
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
...
Because we want to switch launcher icons at runtime, we’ll move the launcher behavior to activity-alias entries instead of keeping it directly on MainActivity.
Step 1 — Remove the LAUNCHER category from MainActivity
We remove the <category android:name=”android.intent.category.LAUNCHER” /> from MainActivity’s intent-filter. This ensures the launcher icon will be controlled by aliases.
So how can we do this? We are going to use the buildActions feature introduced in MABS 12 and create a JSON file like this:
{
"platforms": {
"android": {
"manifest": [
{
"file": "AndroidManifest.xml",
"target": "manifest/application/activity",
"delete": "//category"
}]
}
}
}
Step 2 — Add activity-alias entries to the AndroidManifest
To change the icon at runtime, we add aliases with different icons. Each alias becomes a potential launcher. The aliases are the most crucial part of the Android implementation, and without them we are not able to change the app icon in dynamically. During development, I noticed that changing the enabled launcher alias can cause the app to be force-stopped every time the icon changes. A practical workaround is to include two identical aliases for the original icon:
- one enabled
- one disabled
After the first switch, subsequent changes can happen without forcing the app to close (behavior depends on device/launcher, but this approach significantly improves the experience). As a result, we need to add these two aliases to the AndroidManifest.xml:
<activity-alias
android:label="@string/title_activity_main"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher"
android:name=".originalicon"
android:enabled="true"
android:exported="true"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<activity-alias
android:label="@string/title_activity_main"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher"
android:name=".originaliconclone"
android:enabled="false"
android:exported="true"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
These aliases must be added and not modified for the correct function of the plugin.
Adding aliases for alternative icons
To facilitate this configuration, I’ve come up with a naming convention for icons (resources) and aliases:
- Android mipmap icons: ic_icon1, ic_icon2, …
- Alias names: .icon1, .icon2, …
For this example, I’m going to add two different alternative icons:
<activity-alias
android:label="@string/title_activity_main"
android:icon="@mipmap/ic_icon1"
android:roundIcon="@mipmap/ic_icon1"
android:name=".icon1"
android:enabled="false"
android:exported="true"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<activity-alias
android:label="@string/title_activity_main"
android:icon="@mipmap/ic_icon2"
android:roundIcon="@mipmap/ic_icon2"
android:name=".icon2"
android:enabled="false"
android:exported="true"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
Updating our buildAction, we end up with:
{
"platforms": {
"android": {
"manifest": [
{
"file": "AndroidManifest.xml",
"target": "manifest/application/activity",
"delete": "//category"
},
{
"file": "AndroidManifest.xml",
"target": "manifest/application",
"inject": "<activity-alias\n android:label=\"@string/title_activity_main\"\n android:icon=\"@mipmap/ic_launcher\"\n android:roundIcon=\"@mipmap/ic_launcher\"\n android:name=\".originalicon\"\n android:enabled=\"true\"\n android:exported=\"true\"\n android:targetActivity=\".MainActivity\">\n <intent-filter>\n <action android:name=\"android.intent.action.MAIN\" />\n <category android:name=\"android.intent.category.LAUNCHER\" />\n </intent-filter>\n</activity-alias>\n"
},
{
"file": "AndroidManifest.xml",
"target": "manifest/application",
"inject": "<activity-alias\n android:label=\"@string/title_activity_main\"\n android:icon=\"@mipmap/ic_launcher\"\n android:roundIcon=\"@mipmap/ic_launcher\"\n android:name=\".originaliconclone\"\n android:enabled=\"false\"\n android:exported=\"true\"\n android:targetActivity=\".MainActivity\">\n <intent-filter>\n <action android:name=\"android.intent.action.MAIN\" />\n <category android:name=\"android.intent.category.LAUNCHER\" />\n </intent-filter>\n</activity-alias>\n"
},
{
"file": "AndroidManifest.xml",
"target": "manifest/application",
"inject": "<activity-alias\n android:label=\"@string/title_activity_main\"\n android:icon=\"@mipmap/ic_icon1\"\n android:roundIcon=\"@mipmap/ic_icon1\"\n android:name=\".icon1\"\n android:enabled=\"false\"\n android:exported=\"true\"\n android:targetActivity=\".MainActivity\">\n <intent-filter>\n <action android:name=\"android.intent.action.MAIN\" />\n <category android:name=\"android.intent.category.LAUNCHER\" />\n </intent-filter>\n</activity-alias>\n"
},
{
"file": "AndroidManifest.xml",
"target": "manifest/application",
"inject": "<activity-alias\n android:label=\"@string/title_activity_main\"\n android:icon=\"@mipmap/ic_icon2\"\n android:roundIcon=\"@mipmap/ic_icon2\"\n android:name=\".icon2\"\n android:enabled=\"false\"\n android:exported=\"true\"\n android:targetActivity=\".MainActivity\">\n <intent-filter>\n <action android:name=\"android.intent.action.MAIN\" />\n <category android:name=\"android.intent.category.LAUNCHER\" />\n </intent-filter>\n</activity-alias>\n"
}
]
}
}
}
This way, we have concluded our configurations for Android.
iOS: Changing the app icon dynamically
On iOS, dynamic icons are configured through Info.plist file, using:
- CFBundleIcons
- CFBundlePrimaryIcon
- CFBundleAlternateIcons
The CFBundlePrimaryIcon is the icon that you added as a logo in for you ODC Studio Mobile app, and it will be always with the name AppIcon60x60 (automatically generated by the platform). CFBundleAlternateIcons will be the dictionary of icons that are available to change. In these icons, we are going to follow the same naming convention as in the Android Configurations. In order to achieve this we are going to use buildActions again. So we need to update our previously created JSON file with:
"ios": {
"targets": {
"App": {
"plist": [
{
"replace": false,
"entries": [
{
"CFBundleIcons": {
"CFBundlePrimaryIcon": {
"CFBundleIconFiles": [
"AppIcon60x60"
],
"UIPrerenderedIcon": false
},
"CFBundleAlternateIcons": {
"icon1": {
"CFBundleIconFiles": [
"icon1"
],
"UIPrerenderedIcon": false
},
"icon2": {
"CFBundleIconFiles": [
"icon2"
],
"UIPrerenderedIcon": false
}
}
}
}
]
}
]
}
}
}
Resulting in the final JSON file:
{
"platforms": {
"android": {
"manifest": [
{
"file": "AndroidManifest.xml",
"target": "manifest/application/activity",
"delete": "//category"
},
{
"file": "AndroidManifest.xml",
"target": "manifest/application",
"inject": "<activity-alias\n android:label=\"@string/title_activity_main\"\n android:icon=\"@mipmap/ic_launcher\"\n android:roundIcon=\"@mipmap/ic_launcher\"\n android:name=\".originalicon\"\n android:enabled=\"true\"\n android:exported=\"true\"\n android:targetActivity=\".MainActivity\">\n <intent-filter>\n <action android:name=\"android.intent.action.MAIN\" />\n <category android:name=\"android.intent.category.LAUNCHER\" />\n </intent-filter>\n</activity-alias>\n"
},
{
"file": "AndroidManifest.xml",
"target": "manifest/application",
"inject": "<activity-alias\n android:label=\"@string/title_activity_main\"\n android:icon=\"@mipmap/ic_launcher\"\n android:roundIcon=\"@mipmap/ic_launcher\"\n android:name=\".originaliconclone\"\n android:enabled=\"false\"\n android:exported=\"true\"\n android:targetActivity=\".MainActivity\">\n <intent-filter>\n <action android:name=\"android.intent.action.MAIN\" />\n <category android:name=\"android.intent.category.LAUNCHER\" />\n </intent-filter>\n</activity-alias>\n"
},
{
"file": "AndroidManifest.xml",
"target": "manifest/application",
"inject": "<activity-alias\n android:label=\"@string/title_activity_main\"\n android:icon=\"@mipmap/ic_icon1\"\n android:roundIcon=\"@mipmap/ic_icon1\"\n android:name=\".icon1\"\n android:enabled=\"false\"\n android:exported=\"true\"\n android:targetActivity=\".MainActivity\">\n <intent-filter>\n <action android:name=\"android.intent.action.MAIN\" />\n <category android:name=\"android.intent.category.LAUNCHER\" />\n </intent-filter>\n</activity-alias>\n"
},
{
"file": "AndroidManifest.xml",
"target": "manifest/application",
"inject": "<activity-alias\n android:label=\"@string/title_activity_main\"\n android:icon=\"@mipmap/ic_icon2\"\n android:roundIcon=\"@mipmap/ic_icon2\"\n android:name=\".icon2\"\n android:enabled=\"false\"\n android:exported=\"true\"\n android:targetActivity=\".MainActivity\">\n <intent-filter>\n <action android:name=\"android.intent.action.MAIN\" />\n <category android:name=\"android.intent.category.LAUNCHER\" />\n </intent-filter>\n</activity-alias>\n"
}
]
},
"ios": {
"targets": {
"App": {
"plist": [
{
"replace": false,
"entries": [
{
"CFBundleIcons": {
"CFBundlePrimaryIcon": {
"CFBundleIconFiles": [
"AppIcon60x60"
],
"UIPrerenderedIcon": false
},
"CFBundleAlternateIcons": {
"icon1": {
"CFBundleIconFiles": [
"icon1"
],
"UIPrerenderedIcon": false
},
"icon2": {
"CFBundleIconFiles": [
"icon2"
],
"UIPrerenderedIcon": false
}
}
}
}
]
}
]
}
}
}
}
}
The final result of the plist file will be something similar to this:
...
<dict>
...
<key>CFBundleIcons</key>
<dict>
<key>CFBundlePrimaryIcon</key>
<dict>
<key>CFBundleIconFiles</key>
<array>
<string>AppIcon60x60</string>
</array>
<key>UIPrerenderedIcon</key>
<false/>
</dict>
<key>CFBundleAlternateIcons</key>
<dict>
<key>icon1</key>
<dict>
<key>CFBundleIconFiles</key>
<array>
<string>icon1</string>
</array>
<key>UIPrerenderedIcon</key>
<false/>
</dict>
<key>icon2</key>
<dict>
<key>CFBundleIconFiles</key>
<array>
<string>icon2</string>
</array>
<key>UIPrerenderedIcon</key>
<false/>
</dict>
</dict>
</dict>
</dict>
</plist>
Let's wrap up our final configurations on the ODC Studio side.
Final configurations in ODC Studio
Step 1 — Upload icon resources
Upload your icons in Resources and use the naming convention:
- ic_icon1.png, ic_icon2.png, …
The recommended size for the icons is 1024×1024, because both platforms handle resizing well from a universal asset.

Icons uploaded to the resources
Set Deploy Action to Deploy to the target directory so the files are included in the build output.
Step 2 — Upload the JSON file and reference it in Extensibility Configurations
Upload your JSON(e.g., appIcons.json) to Resources and set it to Deploy to target directory.
Then configure the buildAction in your mobile app’s extensibility configurations:
{
"buildConfigurations": {
"buildAction": {
"config": "$resources.appIcons.json"
}
}
}
Step 3 — Ensure iOS icon files are included in the iOS project
On iOS, the alternate icons must exist in the project bundle with the exact names referenced in Info.plist (icon1, icon2, etc.). Configure iOS resources mapping:
{
"buildConfigurations": {
"buildAction": {
"config": "$resources.appIcons.json"
},
"resources": {
"ios": [
{
"source": "$resources.ic_icon1.png",
"target": "icon1.png"
},
{
"source": "$resources.ic_icon2.png",
"target": "icon2.png"
}
]
}
}
}
Note: the target name matches what you used under CFBundleAlternateIcons (icon1, icon2) plus the file extension.
Using the App Icon Plugin
After adding the App Icon Plugin library to your app, you’ll have three different client actions available. The key ones are:
- ChangeIcon
Inputs:
- Alias: the alias to enable, prefixed with a dot, e.g. .icon1
- Aliases: CSV of available Android aliases (also prefixed with a dot), e.g .icon1,.icon2 (Ignored on iOS)
- ResetIcon
Resets the app icon back to the original one.
Input:
- Aliases: same CSV list used in ChangeIcon client action (Ignored on iOS)

Implementation example for the “ChangeIcon” client action
After all these configurations, we can check what the final result looks like in our app:
iOS Result

iOS Result
Android Result

Android Result
Conclusion
With a Capacitor plugin + MABS 12 Build Actions, you can implement dynamic app icons in ODC for both Android and iOS. The key for this implementation is:
- Android: launcher controlled by activity-alias
- iOS: alternate icons configured in Info.plist
- ODC Studio: consistent resource naming + extensibility configurations
메타데이터
- post_id
- d98febb49344
- slug
- how-to-dynamically-change-your-outsystems-capacitor-mobile-app-icon-in-odc-android-ios-d98febb49344
- url
- https://blog.product-league.com/how-to-dynamically-change-your-outsystems-capacitor-mobile-app-icon-in-odc-android-ios-d98febb49344
- canonical_url
- https://blog.product-league.com/how-to-dynamically-change-your-outsystems-capacitor-mobile-app-icon-in-odc-android-ios-d98febb49344
- author_url
- https://medium.com/@p.m.solipa
- status
- ok
- fetched_at
- 2026-07-09 15:12:33