Chameleon Icons
A cross-platform Flutter plugin and automated CLI tool to dynamically change app launcher icons at runtime on Android and iOS with zero manual native boilerplate.
Overview
Dynamically changing app icons has traditionally been complex and error-prone:
- Android: Requires manual
<activity-alias>declarations inAndroidManifest.xml, multi-densitymipmapgeneration (mdpitoxxxhdpi), and complexPackageManagercomponent-state manipulation. - iOS: Requires manual configuration of nested
CFBundleAlternateIconsdictionaries inInfo.plist, retina@2x/@3xasset linking, andUIApplication.setAlternateIconNamehandling.
Chameleon Icons solves both sides:
- Unified Dart API: A clean, idiomatic Flutter API (
changeIcon,resetIcon,getCurrentIcon,isAlternateIconsSupported) that feels natural to Flutter developers. - Automated CLI Engine (Coming in v0.1.0): Define icons once in
pubspec.yaml, rundart run chameleon_icons:generate, and let the CLI resize all densities and inject all Android XML and iOS Plist configurations automatically.
Platform Support
| Platform | Supported | Switching Engine |
|---|---|---|
| Android | Yes (API 24+) | PackageManager.setComponentEnabledSetting via <activity-alias> |
| iOS | Yes (iOS 15.0+) | UIApplication.shared.setAlternateIconName(_:) via CFBundleAlternateIcons |
Installation
Add chameleon_icons to your pubspec.yaml:
dependencies:
chameleon_icons: ^0.0.1
Then run:
flutter pub get
Platform Setup (v0.0.1 Manual Setup)
Note: In version 0.1.0, this setup will be 100% automated using
dart run chameleon_icons:generate. For version0.0.1, follow the manual setup below.
1. Android Configuration
Open android/app/src/main/AndroidManifest.xml and make the following adjustments:
Step A: Remove the launcher intent filter from <activity android:name=".MainActivity">
Comment out or remove the <intent-filter> from your main activity so it is not launched directly without an alias:
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<!-- REMOVE or COMMENT OUT the launcher intent-filter from MainActivity:
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
-->
</activity>
Step B: Declare the default icon metadata
Inside the <application> tag, declare the name of your primary default alias:
<application ...>
<!-- Required: Tells ChameleonIcons which alias represents the default icon -->
<meta-data
android:name="com.farookjamal.chameleon_icons.DEFAULT_ICON_ALIAS"
android:value="MainActivityDefault" />
<!-- ... activities ... -->
Step C: Add <activity-alias> for Default and Alternate Icons
Directly below <activity android:name=".MainActivity">, define an <activity-alias> for each icon:
<!-- Primary Default Icon (Must have android:enabled="true") -->
<activity-alias
android:name=".MainActivityDefault"
android:enabled="true"
android:exported="true"
android:icon="@mipmap/ic_launcher"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<!-- Alternate Icon 1: Dark (Must have android:enabled="false") -->
<activity-alias
android:name=".MainActivityDark"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_launcher_dark"
android:roundIcon="@mipmap/ic_launcher_dark_round"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<!-- Alternate Icon 2: Gold (Must have android:enabled="false") -->
<activity-alias
android:name=".MainActivityGold"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_launcher_gold"
android:roundIcon="@mipmap/ic_launcher_gold_round"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
</application>
2. iOS Configuration
Step A: Add PNG Assets to your iOS App Bundle
Add your alternate icon PNG files inside ios/Runner/ at @2x (120×120 px) and @3x (180×180 px) resolutions:
ios/Runner/dark_icon@2x.pngios/Runner/dark_icon@3x.pngios/Runner/gold_icon@2x.pngios/Runner/gold_icon@3x.png
Important: In Xcode, ensure these image files are checked under Target Membership -> Runner.
Step B: Configure Info.plist
Open ios/Runner/Info.plist and add the CFBundleIcons dictionary under the root <dict> tag:
<key>CFBundleIcons</key>
<dict>
<!-- Alternate Icons Configuration -->
<key>CFBundleAlternateIcons</key>
<dict>
<key>MainActivityDark</key>
<dict>
<key>CFBundleIconFiles</key>
<array>
<string>dark_icon</string>
</array>
<key>UIPrerenderedIcon</key>
<false/>
</dict>
<key>MainActivityGold</key>
<dict>
<key>CFBundleIconFiles</key>
<array>
<string>gold_icon</string>
</array>
<key>UIPrerenderedIcon</key>
<false/>
</dict>
</dict>
<!-- Primary Default Icon Configuration -->
<key>CFBundlePrimaryIcon</key>
<dict>
<key>CFBundleIconFiles</key>
<array>
<string>AppIcon</string>
</array>
</dict>
</dict>
Dart API Usage
import 'package:flutter/material.dart';
import 'package:chameleon_icons/chameleon_icons.dart';
final chameleon = ChameleonIcons();
// 1. Check if alternate icons are supported on the device:
final isSupported = await chameleon.isAlternateIconsSupported();
if (isSupported) {
// 2. Switch to an alternate icon:
await chameleon.changeIcon("MainActivityDark");
// 3. Query the currently active icon:
final currentIcon = await chameleon.getCurrentIcon();
print("Active icon: $currentIcon"); // Outputs: "MainActivityDark"
// 4. Reset back to the primary default app icon:
await chameleon.resetIcon();
}
Platform Comparison
| Feature | Android Implementation | iOS Implementation |
|---|---|---|
| Switching Mechanism | <activity-alias> with PackageManager.setComponentEnabledSetting |
UIApplication.shared.setAlternateIconName(_:) |
| Default Icon Reset | Re-enables default launcher alias and disables others | Passes nil to setAlternateIconName(nil) |
| Current Icon Detection | Resolves active launcher intent from PackageManager |
Inspects alternateIconName or CFBundlePrimaryIcon |
| Asset Location | android/app/src/main/res/mipmap-* |
ios/Runner/ bundle resources |
| Min Supported OS | Android SDK 24+ (Android 7.0+) | iOS 15.0+ |
Roadmap
- Phase 1 (v0.0.1): Core native runtime engine for Android & iOS with decoupled
changeIcon()andresetIcon(). (Completed) - Phase 2 (v0.1.0): Automated CLI image generation (
dart run chameleon_icons:generate) for automatic multi-density asset resizing. - Phase 3 (v0.1.0): Automated injection into
AndroidManifest.xmlandInfo.plist. - Phase 4 (v0.2.0): Ready-to-use customizable UI components (Telegram-style
ChameleonIconGrid).
License
MIT License. Copyright (c) 2026 Farook Jamal.