🚀 How I Built Icora — A Small Experiment That Turned Into a Lesson on Dynamic Android Icons

Icora

Changing the launcher icon dynamically is one of those common questions in the Android community:

“Can I update my app icon at runtime?”

The short answer: Yes — but only if the icons are pre-packaged.
Android doesn’t allow replacing launcher icons with downloaded images, but it does allow switching between multiple predefined icons using activity-alias.

To explore this idea and help developers learn the correct implementation, I built Icora, a lightweight Android demo app that switches app icons instantly using Jetpack Compose + activity aliases.

This blog walks through the full implementation.

🎯 Why Icora Exists

A lot of developers try to replace launcher icons using Bitmaps, downloads, or runtime assets — but Android blocks those approaches.
The proper solution is:

  • Prepackage multiple icons
  • Assign each one to an <activity-alias>
  • Enable only one alias at a time
  • Disable all others

Icora demonstrates this cleanly with Compose buttons for each icon.

🧩 How Dynamic Icons Actually Work

Android launchers show app icons based on:

android:icon=""
android:roundIcon=""

These must reference resources inside the APK.

Instead of modifying these at runtime, Icora defines many launchable aliases, each with a different icon:

MainActivityAliasA → icon A  
MainActivityAliasB → icon B
MainActivityAliasC → icon C
...

All aliases point to the same activity (MainActivity), but only one is enabled at a time.

🗂 Manifest Setup (activity-alias)

Below is the essential part of the manifest:

<activity
android:name=".MainActivity"
android:exported="true" />

<activity-alias
android:name=".MainActivityAliasA"
android:enabled="true"
android:exported="true"
android:icon="@mipmap/ic_launcher_a"
android:roundIcon="@mipmap/ic_launcher_a_round"
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:name=".MainActivityAliasB"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_launcher_b"
android:roundIcon="@mipmap/ic_launcher_b_round"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>

Repeat for C, D, E, F…

🔧 Switching Icons Using PackageManager

The heart of the project is this Kotlin function:

fun setAppIcon(context: Context, aliasClassName: String) {
val pm = context.packageManager
val packageName = context.packageName

val aliases = listOf(
"$packageName.MainActivityAliasA",
"$packageName.MainActivityAliasB",
"$packageName.MainActivityAliasC",
"$packageName.MainActivityAliasD",
"$packageName.MainActivityAliasE",
"$packageName.MainActivityAliasF",
"$packageName.MainActivityAliasDefault",
)

for (alias in aliases) {
val component = ComponentName(packageName, alias)
val state = if (alias == "$packageName.$aliasClassName") {
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
} else {
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
}
pm.setComponentEnabledSetting(
component,
state,
PackageManager.DONT_KILL_APP
)
}
}

This function:

  • Enables the selected alias
  • Disables all others
  • Ensures only one launcher icon is visible

🎨 UI Implementation with Jetpack Compose

Icora uses a very simple Compose UI to trigger icon changes:

OptIn(ExperimentalFoundationApi::class, DelicateCoroutinesApi::class)
@Composable
fun FiveButtonGrid() {
val buttons = listOf("A", "B", "C", "D", "E","F")
val context = LocalContext.current

LazyVerticalGrid(
columns = GridCells.Fixed(2),
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(buttons.size) { index ->
Button(
onClick = {
val alias = when(index) {
0-> "MainActivityAliasA"
1-> "MainActivityAliasB"
2-> "MainActivityAliasC"
3-> "MainActivityAliasD"
4-> "MainActivityAliasE"
5-> "MainActivityAliasF"
else -> {
"MainActivityAliasDefault"
}
}
CustomAppIcon.setAppIcon(context,alias)
},
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
) {
Text(text = buttons[index])
}
}
}
}

Each button enables one alias → instantly switching the icon.

🔄 How Fast Does the Icon Update?

Depending on the launcher:

  • Pixel Launcher: 1–2 seconds
  • Samsung One UI: 2–5 seconds
  • Xiaomi MIUI: may require refreshing home screen

🧪 Common Issues & Fixes

✔️ Multiple icons appearing?

Make sure:

  • Only one alias has android:enabled="true" initially
  • The main <activity> does NOT have the LAUNCHER intent-filter

✔️ Icon not updating?

  • Launcher is caching — wait a moment
  • Try dragging the app icon on home screen
  • Some OEM launchers refresh slower

📥 Try Icora or Fork the Project

Feel free to explore, fork, and experiment with Icora.
It’s a great starting point for:

  • Theming apps
  • Seasonal icon changes
  • Premium icon packs
  • Customizable launcher appearance

🎉 Final Thoughts

Dynamic icon switching on Android is a powerful feature — but often misunderstood. Icora demonstrates the proper, stable, and officially supported way to do it.

If you’re building a customizable Android experience, this technique will open up a lot of possibilities.

📦 Full Source Code

Explore the complete implementation of Icora. https://github.com/ajithvgiri/icora

✨ Watch Icora Switch Icons Instantly