chameleon_icons 0.0.1
chameleon_icons: ^0.0.1 copied to clipboard
A Flutter plugin to dynamically change and reset app launcher icons at runtime on Android and iOS.
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.