dynamic_app_icon_flutter_plus 2.1.0
dynamic_app_icon_flutter_plus: ^2.1.0 copied to clipboard
Dynamic App Icon for Flutter enables Flutter apps to change their launcher icon at runtime, ideal for branding updates, seasonal themes, and feature-based icon switching.
dynamic_app_icon_flutter_plus #
A Flutter plugin for dynamically changing app icons on Android and iOS.
|
Android |
iOS |
Features đ #
- đ¨ Android Support: Change app icons using activity aliases
- đ iOS Support: Use alternate app icons (iOS 10.3+)
- đ Auto-Discovery: Automatically finds all available icons from configuration
- đ No Hardcoding: Fully dynamic, client-driven configuration
- đą Cross-Platform: Works seamlessly on both Android and iOS
Requirements â #
- Flutter:
>=3.44.0(2.0.0and later) - Android: Uses Flutter's built-in Kotlin, so the plugin no longer applies the Kotlin Gradle Plugin and builds cleanly on Android Gradle Plugin (AGP) 9.0+.
minSdk 24. - iOS: iOS 10.3+ (Swift Package Manager and CocoaPods both supported).
Still on Flutter
<3.44? Pindynamic_app_icon_flutter_plus: ^1.0.1â the1.xline keeps the older Gradle setup.
Installation #
Add this to your package's pubspec.yaml file:
dependencies:
dynamic_app_icon_flutter_plus: ^2.1.0
Then run:
flutter pub get
Quick Start đ #
Let's get you started with dynamic app icons in no time!
import 'package:dynamic_app_icon_flutter_plus/dynamic_app_icon_flutter_plus.dart';
// Check if dynamic icons are supported on this platform
bool supported = await DynamicAppIconFlutterPlus.supportsAlternateIcons;
// Get all available icons
List<String> icons = await DynamicAppIconFlutterPlus.getAvailableIcons();
// Get the currently active icon
String? currentIcon = await DynamicAppIconFlutterPlus.getAlternateIconName();
// Change to a different icon
await DynamicAppIconFlutterPlus.setAlternateIconName('dark');
// Restore default icon
await DynamicAppIconFlutterPlus.setAlternateIconName(null);
That's it! You're ready to change app icons dynamically. đ
API Reference đ #
supportsAlternateIcons #
Check if the current platform supports dynamic app icons.
Returns: Future<bool> - true if supported, false otherwise
Example:
bool supported = await DynamicAppIconFlutterPlus.supportsAlternateIcons;
if (supported) {
print('Dynamic icons are supported!');
} else {
print('Dynamic icons not supported on this platform');
}
Platform Support:
- â
Android: Always returns
true - â
iOS: Returns
trueon iOS 10.3+,falseotherwise
getAlternateIconName() #
Get the name of the currently active alternate icon.
Returns: Future<String?> - The icon name if an alternate icon is active, null if using the default icon
Example:
String? currentIcon = await DynamicAppIconFlutterPlus.getAlternateIconName();
if (currentIcon == null) {
print('Using default icon');
} else {
print('Current icon: $currentIcon');
}
Note: Returns null when the default app icon is active.
getAvailableIcons() #
Get a list of all available alternate icon names that can be used.
Returns: Future<List<String>> - List of available icon names
Example:
List<String> availableIcons = await DynamicAppIconFlutterPlus.getAvailableIcons();
print('Available icons: $availableIcons');
// Output: Available icons: [dark, light]
Platform Behavior:
- Android: Dynamically discovers all activity aliases from
AndroidManifest.xmlthat follow the patternpackage.MainActivity.iconName - iOS: Reads icon names from
Info.plistunderCFBundleAlternateIcons
Note: The "default" alias is automatically excluded from this list on Android.
setAlternateIconName(iconName, {showAlert, deferUntilBackground}) #
Set the app icon to the specified alternate icon, or restore the default icon.
Parameters:
iconName(String?): The name of the icon to set, ornullto restore the default iconshowAlert(bool, optional): iOS only - Whether to show the system alert when changing icons. Defaults totrue.deferUntilBackground(bool, optional): Android only - Whentrue, the swap is queued and applied once the app is backgrounded instead of immediately. This avoids the "kick to home" some OEM launchers trigger (see Deferred icon swap on Android below). Defaults tofalse(immediate swap, original behavior). No effect on iOS.
Returns: Future<void>
Throws: PlatformException if the icon name doesn't exist or there's an error
Example:
// Set an alternate icon (applies immediately)
await DynamicAppIconFlutterPlus.setAlternateIconName('dark');
// Set icon without alert on iOS (use at your own risk)
await DynamicAppIconFlutterPlus.setAlternateIconName('light', showAlert: false);
// Android: queue the swap and apply it when the app is backgrounded
await DynamicAppIconFlutterPlus.setAlternateIconName('dark', deferUntilBackground: true);
// Restore default icon
await DynamicAppIconFlutterPlus.setAlternateIconName(null);
Platform Behavior:
- Android: Enables the corresponding activity alias and disables others. With
deferUntilBackground: truethis is deferred until the app leaves the foreground. - iOS: Changes the app icon using
UIApplication.setAlternateIconName()
iOS Alert Suppression:
The showAlert parameter uses a private API on iOS to suppress the system alert. Use this at your own risk as it may break in future iOS versions.
applyPendingIcon() #
Android only. Immediately apply an icon swap that was queued with setAlternateIconName(..., deferUntilBackground: true). Call it when your app moves to the background so the swap takes effect right away (e.g. the instant the user presses Home) rather than waiting until the task is removed from recents. Safe to call when there is no pending change. No-op on iOS.
Returns: Future<void>
Example:
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.paused) {
DynamicAppIconFlutterPlus.applyPendingIcon();
}
}
// ...
}
Platform Setup đ§ #
Android Setup #
1. Configure AndroidManifest.xml
Add activity aliases for each alternate icon. The plugin automatically discovers all aliases following the pattern: package.MainActivity.iconName
Important: The main activity must have exported="true" and a launcher intent-filter to serve as the default icon.
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="Your App Name"
android:icon="@mipmap/ic_launcher">
<!-- Main Activity - Used as default icon launcher -->
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Alternate icon 1 -->
<activity-alias
android:name="com.example.yourapp.MainActivity.dark"
android:targetActivity=".MainActivity"
android:exported="true"
android:icon="@mipmap/ic_launcher_dark"
android:label="@string/app_name"
android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity-alias>
<!-- Alternate icon 2 - Add as many as you need -->
<activity-alias
android:name="com.example.yourapp.MainActivity.light"
android:targetActivity=".MainActivity"
android:exported="true"
android:icon="@mipmap/ic_launcher_light"
android:label="@string/app_name"
android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity-alias>
</application>
</manifest>
2. Create Icon Resources
For each alternate icon, create icon files in all density folders. You must provide icons for all density variants:
![]()
-
Navigate to your Android app's
resdirectory (e.g.,android/app/src/main/res/) -
For each alternate icon, create icon files in all density folders:
mipmap-mdpi/ic_launcher_{iconName}.pngmipmap-hdpi/ic_launcher_{iconName}.pngmipmap-xhdpi/ic_launcher_{iconName}.pngmipmap-xxhdpi/ic_launcher_{iconName}.pngmipmap-xxxhdpi/ic_launcher_{iconName}.png
-
Example structure for multiple icons:
res/ mipmap-mdpi/ ic_launcher.png (default icon - required) ic_launcher_dark.png (alternate icon) ic_launcher_light.png (alternate icon) mipmap-hdpi/ ic_launcher.png ic_launcher_dark.png ic_launcher_light.png mipmap-xhdpi/ ic_launcher.png ic_launcher_dark.png ic_launcher_light.png mipmap-xxhdpi/ ic_launcher.png ic_launcher_dark.png ic_launcher_light.png mipmap-xxxhdpi/ ic_launcher.png ic_launcher_dark.png ic_launcher_light.png
Important:
- Replace
{iconName}with your actual icon name (e.g.,dark,light) - The icon name in the filename must match the name used in the
activity-aliasinAndroidManifest.xml - Ensure all density variants are provided for each icon
- The default icon (
ic_launcher.png) is required and should be present in all density folders
iOS Setup #
Configure Info.plist
Add alternate icons configuration to your Info.plist. Important: You must include both the alternate icons (CFBundleAlternateIcons) and the default/primary icon (CFBundlePrimaryIcon) configuration:
<key>CFBundleIcons</key>
<dict>
<key>CFBundleAlternateIcons</key>
<dict>
<key>dark</key>
<dict>
<key>CFBundleIconFiles</key>
<array>
<string>dark</string>
</array>
<key>UIPrerenderedIcon</key>
<false/>
</dict>
<key>light</key>
<dict>
<key>CFBundleIconFiles</key>
<array>
<string>light</string>
</array>
<key>UIPrerenderedIcon</key>
<false/>
</dict>
</dict>
<key>CFBundlePrimaryIcon</key>
<dict>
<key>CFBundleIconFiles</key>
<array>
<string>default</string>
</array>
<key>UIPrerenderedIcon</key>
<false/>
</dict>
</dict>
<key>CFBundleIcons~ipad</key>
<dict>
<key>CFBundleAlternateIcons</key>
<dict>
<key>dark</key>
<dict>
<key>CFBundleIconFiles</key>
<array>
<string>dark</string>
</array>
<key>UIPrerenderedIcon</key>
<false/>
</dict>
<key>light</key>
<dict>
<key>CFBundleIconFiles</key>
<array>
<string>light</string>
</array>
<key>UIPrerenderedIcon</key>
<false/>
</dict>
</dict>
<key>CFBundlePrimaryIcon</key>
<dict>
<key>CFBundleIconFiles</key>
<array>
<string>default</string>
</array>
<key>UIPrerenderedIcon</key>
<false/>
</dict>
</dict>
Note: The CFBundlePrimaryIcon configuration for the default icon is required. Make sure to include it in both CFBundleIcons (for iPhone) and CFBundleIcons~ipad (for iPad) sections. The default icon name should match the icon files you place in the App Icon folder (e.g., default@2x.png and default@3x.png).
Add Icon Assets
![]()
-
Open your project in Xcode
-
Create an
App Iconfolder in yourRunnerdirectory (e.g.,ios/Runner/App Icon/) -
Add icon image files for each icon (including the required default icon) with the following naming convention:
- For each icon name (e.g.,
dark,light,default), add:{iconName}@2x.png(120x120 pixels for iPhone){iconName}@3x.png(180x180 pixels for iPhone)
Important: You must include the default icon files. Example files:
default@2x.png(required - for default/primary icon)default@3x.png(required - for default/primary icon)dark@2x.png(for alternate icon)dark@3x.png(for alternate icon)light@2x.png(for alternate icon)light@3x.png(for alternate icon)
- For each icon name (e.g.,
-
Ensure the icon names in your files match the keys used in
Info.plist(e.g., if you use"dark"in Info.plist, name your filesdark@2x.pnganddark@3x.png) -
Open
Runner.xcworkspacein Xcode (notRunner.xcodeproj) -
Right-click on
Runnerin the project navigator and select "Add Files to 'Runner'"
-
Select the
App Iconfolder you created in step 2
-
In the dialog that appears, select "Create folder references" (not "Create groups") and click "Add"

Requirements:
- iOS 10.3 or later
- Icon files must be placed in the
App Iconfolder within your Runner directory - Provide both @2x and @3x versions for each alternate icon
Build System (Swift Package Manager & CocoaPods)
This plugin supports both of Flutter's iOS build systems, so no extra setup is needed either way:
- Swift Package Manager (SPM) â fully supported (since
1.0.1). If you've enabled SPM (flutter config --enable-swift-package-manager), the plugin is resolved as a Swift package and you will not see the "does not support Swift Package Manager" warning. - CocoaPods â still fully supported as the default/fallback. No changes required for existing projects.
âšī¸ If you previously used
1.0.0and saw the SPM compatibility warning, upgrade to1.0.1â the Swift package layout was corrected so Flutter now detects it.
Deferred icon swap on Android đ˛ #
On Android, swapping the icon enables/disables an activity-alias, which fires a PACKAGE_CHANGED broadcast. Several OEM launchers (MIUI, EMUI/HarmonyOS, OneUI, ColorOS) react to that broadcast while your app is in the foreground by refreshing and kicking the user back to the home screen mid-session.
To avoid this, you can defer the swap until the app is backgrounded:
// 1. Queue the swap instead of applying it immediately
await DynamicAppIconFlutterPlus.setAlternateIconName('dark', deferUntilBackground: true);
// 2. Flush it the moment the app goes to background (recommended)
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.paused) {
DynamicAppIconFlutterPlus.applyPendingIcon();
}
}
}
How the deferred path behaves:
- The queued change is applied when the app is backgrounded via
applyPendingIcon(), or â if you don't call it â when the user swipes the app from recents (handled by an internalDynamicAppIconService), or on the next app launch as a fallback if the process was killed first. getAlternateIconName()returns the pending value immediately, so your UI reflects the user's choice even before the swap physically happens.
This is opt-in. Leave deferUntilBackground at its default (false) to keep the original immediate-swap behavior. applyPendingIcon() is a no-op on iOS, so it's safe to call unconditionally in cross-platform code.
Complete Example đĄ #
Here's a complete example showing how to build an icon selector:
import 'package:flutter/material.dart';
import 'package:dynamic_app_icon_flutter_plus/dynamic_app_icon_flutter_plus.dart';
class IconSelector extends StatefulWidget {
@override
_IconSelectorState createState() => _IconSelectorState();
}
class _IconSelectorState extends State<IconSelector> {
List<String> availableIcons = [];
String? currentIcon;
bool isLoading = true;
@override
void initState() {
super.initState();
_loadIcons();
}
Future<void> _loadIcons() async {
final icons = await DynamicAppIconFlutterPlus.getAvailableIcons();
final current = await DynamicAppIconFlutterPlus.getAlternateIconName();
setState(() {
availableIcons = icons;
currentIcon = current;
isLoading = false;
});
}
Future<void> _setIcon(String? iconName) async {
try {
await DynamicAppIconFlutterPlus.setAlternateIconName(iconName);
await _loadIcons();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Icon changed successfully!')),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: $e')),
);
}
}
@override
Widget build(BuildContext context) {
if (isLoading) {
return Center(child: CircularProgressIndicator());
}
return ListView(
children: [
// Default icon option
ListTile(
title: Text('Default'),
trailing: currentIcon == null ? Icon(Icons.check) : null,
onTap: () => _setIcon(null),
),
// Alternate icons
...availableIcons.map((iconName) => ListTile(
title: Text(iconName),
trailing: currentIcon == iconName ? Icon(Icons.check) : null,
onTap: () => _setIcon(iconName),
)),
],
);
}
}
For a complete example with a beautiful UI, check out the example directory.
How It Works đ #
Android #
The plugin uses Android's activity aliases feature:
- Each alternate icon is configured as an
activity-aliastargeting the main activity - The plugin dynamically discovers all aliases from
AndroidManifest.xml - When changing icons, it enables the target alias and disables others
- The main activity serves as the default icon launcher
iOS #
The plugin uses iOS's alternate app icons feature:
- Alternate icons are configured in
Info.plistunderCFBundleAlternateIcons - The plugin reads available icons from the Info.plist
- Uses
UIApplication.setAlternateIconName()to change icons - Returns
nullwhen the default icon is active
Notes â ī¸ #
- Android: After changing the icon, users may need to go to the home screen to see the change
- Android: The icon change happens immediately by default. Pass
deferUntilBackground: trueto delay it until the app is backgrounded and avoid the OEM-launcher "kick to home" â see Deferred icon swap on Android - iOS: Changing icons may show a system alert (can be suppressed with
showAlert: false) - iOS: Requires iOS 10.3 or later
- iOS Simulator: Changing the icon often fails on the Simulator with
PlatformException(Failed to set icon, ... Resource temporarily unavailable). This is a known iOS Simulator bug (POSIXEAGAIN), not a plugin issue â test alternate icons on a real device, where it works as expected. - iOS:
showAlert: falseuses a private API to suppress the alert. This is fine for testing and personal builds, but may lead to App Store rejection â keepshowAlert: truefor production releases. - iOS: Works with both Swift Package Manager and CocoaPods (no extra configuration needed).
- All activity aliases (Android) must target the same MainActivity
- Only one icon can be active at a time
License #
MIT License - see LICENSE file for details.