adaptive_app_icon

Switch your app's home-screen icon at runtime between pre-bundled variants — with a config-driven codegen system, a type-safe Pigeon bridge, and a drop-in gallery widget for icon selection.

Switching the app icon at runtime on Android and iOS

Android (left) and iOS (right): picking a variant from IconGallery, the iOS system confirmation alert, and the new icon on the home screen. Full-resolution video →

iOS UIApplication.setAlternateIconName(_:) (iOS 10.3+)
Android <activity-alias> toggling via PackageManager.setComponentEnabledSetting

Important

This package only switches between icons that are already bundled in the binary at build time. Neither iOS nor Android can download a new icon at runtime — every variant must ship inside the app. Adding a new icon later requires a new build and store submission.


How it works (and its limits)

  • iOS shows an unavoidable system confirmation alert every time the icon changes. This is enforced by the OS and cannot be suppressed. If the user cancels, the switch fails and the gallery selection rolls back automatically.

  • Android enables exactly one <activity-alias> at a time (the main activity is never toggled). Because Android finishes the current task when the launcher component it was launched through is disabled, a switch can't happen silently mid-session. Choose how it lands with androidApplyMode:

    AndroidApplyMode Behavior
    whenBackgrounded (default) Applied when the app is next backgrounded — the app never visibly closes; the icon is already updated by the time the user is on the home screen.
    immediately Applied now; Android closes the app and the user reopens it. Pair it with IconGallery.confirmChange to warn the user first.

    iOS ignores androidApplyMode entirely.

    Why no auto-relaunch? Android 10+ blocks background activity starts, so an app cannot reliably relaunch itself after being closed. Prefer whenBackgrounded (no close at all), or use immediately and tell the user to reopen the app.


Quick start (3 steps)

1. Drop your icon art in assets/ and declare it in pubspec.yaml

One square PNG per icon — 1024×1024 is the recommended master size. Declare them as Flutter assets, then list the variants:

flutter:
  assets:
    - assets/icons/

adaptive_app_icon:
  icons:
    # The first entry (or one with `default: true`) is the primary icon.
    - name: classic
      image: assets/icons/classic.png
      label: "Classic"

    - name: neon
      image: assets/icons/neon.png
      label: "Neon"

That is the whole config for the common case — every platform asset is rendered from image:. name must be lower_snake_case: it becomes an iOS app-icon set name, an Android component name, and the string you pass to setIcon.

Optional keys
Key Scope Default
label per icon the name
default per icon first entry in the list
background per icon or top level #ffffff — composited under the iOS icon, which cannot have an alpha channel
preview per icon the image — override to show different art in the gallery
android_asset per icon ic_launcher for the default, ic_launcher_<name> otherwise

Omit image: on an icon to keep hand-maintained assets instead: supply ios/Runner/Assets.xcassets/<name>.appiconset and an android_asset resource yourself, and codegen will wire them up without rendering anything.

The config can also live in a standalone adaptive_app_icon.yaml at the project root; pubspec.yaml wins if both declare an adaptive_app_icon: section.

2. Run the codegen

dart run adaptive_app_icon:generate_icon_config

This automatically:

  • Renders res/mipmap-*/ launcher PNGs at all five densities (mdpi→xxxhdpi), preserving alpha so launchers can mask them.
  • Renders ios/Runner/Assets.xcassets/<name>.appiconset for each icon, flattened onto background because iOS renders an icon with an alpha channel as a black square.
  • Points the Xcode asset-catalog build settings (ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS and ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES) at those sets on every build configuration, so alternates are bundled without touching Copy Bundle Resources — and Xcode writes CFBundleAlternateIcons into the built Info.plist itself.
  • Injects <activity-alias> blocks into android/app/src/main/AndroidManifest.xml (one enabled default, the rest disabled), wrapped in adaptive_app_icon:begin/end markers so re-runs stay idempotent.
  • Generates lib/app_icons.g.dart — a typed List<AppIconAsset> plus the Android component map and an initDynamicAppIcon() helper. No raw strings downstream.
  • Validates loudly: unreadable sources, sources below 1024px, names that aren't valid platform identifiers, and previews that exist but aren't declared under flutter: assets: (a silent placeholder at runtime otherwise).

Re-run it any time you change the config or the art. Useful flags: --dry-run, --verbose, --no-images (wire up existing assets without re-rendering), and --project <dir>.

import 'package:adaptive_app_icon/adaptive_app_icon.dart';
import 'app_icons.g.dart'; // generated

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  initDynamicAppIcon(); // register the generated config
  runApp(const MyApp());
}

// ...on a settings screen:
FutureBuilder<String?>(
  future: DynamicAppIcon.getCachedIcon(), // avoids a cold-start flash
  builder: (context, snapshot) => IconGallery(
    icons: DynamicAppIcon.icons,
    initialSelectedName: snapshot.data,
    onChanged: (icon) => debugPrint('Switched to ${icon.label}'),
  ),
);

Or call the API directly:

await DynamicAppIcon.setIcon('neon');       // switch
await DynamicAppIcon.setIcon(null);         // back to primary
final current = await DynamicAppIcon.getCurrentIcon(); // 'neon' or null
final ok = await DynamicAppIcon.isSupported();

What the codegen handles vs. what you must do manually

Handled automatically You must do manually
Android density PNGs and iOS app-icon sets, rendered from one source each Draw the art — one square PNG per icon (the codegen never invents art)
Xcode asset-catalog build settings, so alternates bundle without Copy Bundle Resources surgery Declare the assets folder under flutter: assets: so gallery previews load
AndroidManifest.xml aliases (incl. moving the launcher off MainActivity)
Typed Dart config
Source, name, and preview validation

What lands where

  • iOS. Every icon becomes an app-icon set in Assets.xcassets: the default takes over AppIcon.appiconset, and each alternate gets <name>.appiconset. Naming the set after the icon is deliberate — the name Xcode registers in CFBundleAlternateIcons is then the same string you pass to setIcon, so nothing has to be translated at runtime. Sets are written in the single-size (1024) Xcode 14+ format and the asset compiler derives the device sizes.

    Icons are flattened onto background because iOS renders an icon with an alpha channel as a black square. Set background: to match your art; the default is white.

  • Android. Each icon is rendered to res/mipmap-<density>/<android_asset>.png at all five densities with alpha intact. The codegen turns every icon into an <activity-alias> and removes the LAUNCHER intent-filter from your MainActivity, so the currently-running activity is never disabled during a switch (doing so crashes the app). Icons only get legacy launcher PNGs today — adaptive icons (mipmap-anydpi-v26/*.xml) are not generated, though you can hand-maintain them and point android_asset at them.

Gallery previews default to the same image: you already declared, so there is no fourth file to keep in sync. Override preview: only when you want the in-app thumbnail to differ from the launcher art.

Migrating from the loose-PNG layout

Earlier versions bundled alternates as loose AppIcon-Neon@2x.png files in ios/Runner/ and needed a manual Copy Bundle Resources step. If you are coming from that:

  1. Delete the ios_asset: keys — codegen now errors on them, since app-icon set names are derived. Add image: to each icon instead.
  2. Delete the loose AppIcon-*@2x.png / @3x.png files in Xcode (which also removes them from Copy Bundle Resources). Codegen warns while any remain.
  3. Re-run the codegen. It also strips the CFBundleIcons block it used to write into ios/Runner/Info.plist, which now points at files that no longer exist.

Known limitations & review notes

Note

Android notification icons are separate. The small icon shown in the status bar / notifications is set independently of the launcher icon and will not follow an icon switch. Update it via your notification code if needed.

Warning

iOS App Store review. Every bundled icon variant is visible to reviewers at submission time (they appear in the "App Icons" section). Icons cannot be added over-the-air — introducing a new variant later requires a new build and a new review.

Other notes:

  • iOS always shows a confirmation alert on switch; there is no API to suppress it.
  • iPad. This package writes the iPhone CFBundleIcons entry. For dedicated iPad alternate icons add a CFBundleIcons~ipad entry with 152/167 px assets.
  • Android package assumption. Generated component names use your module's namespace. If your applicationId differs from your namespace, verify the alias names resolve on-device.

API reference

DynamicAppIcon

Member Description
initialize({icons, androidComponents}) Register the generated config. Call once at startup (via initDynamicAppIcon()).
setIcon(String? name, {AndroidApplyMode androidApplyMode}) Activate name, or the primary icon when null. See the apply-mode table above.
getCurrentIcon() Active icon name, or null for the primary icon.
isSupported() Whether switching is available (platform supports it and >1 icon configured).
getCachedIcon() Last selection from shared_preferences — seed cold-start UI with this.
icons / defaultIconName The registered icon list / the default icon's name.

Failures throw a PlatformException with code UNSUPPORTED, BAD_ARGS, or SET_FAILED.

IconGallery

A GridView that renders preview thumbnails, checkmarks the active icon, and performs an optimistic switch that rolls back if setIcon throws (e.g. the user cancels iOS's alert). Provide itemBuilder to fully restyle each tile without forking the widget.


Regenerating the native bridge

The Dart⇄Swift⇄Kotlin bindings are generated by Pigeon from pigeons/messages.dart:

dart run pigeon --input pigeons/messages.dart

Example

See example/ for a working app with three icon variants, the gallery wired onto a settings screen, and cold-start state restoration.

Libraries

adaptive_app_icon
Runtime app-icon switching for Flutter, with config-driven codegen and a drop-in gallery widget.