π Bee Dynamic Launcher
JSON-driven launcher icons for Flutter β change the home-screen app icon on Android and iOS from Dart, and optionally use a CLI so your Android and iOS projects stay aligned with the same
catalog.jsonand icon assets.
On this page: Screenshots Β· Overview Β· Platform Β· Requirements Β· Getting started Β· CLI Β· Android Β· iOS Β· API Β· Troubleshooting
π― Screenshots
Demo (GIF)
| iOS | Android |
|---|---|
![]() |
![]() |
Launcher Result
| iOS launcher | Android launcher |
|---|---|
![]() |
![]() |
iOS System Confirmation
π Overview
| You need | This package provides |
|---|---|
| Multiple branded / seasonal launcher icons | One catalog.json + ic_<id>.png masters per variant |
Previews inside Flutter (Image.asset) |
LauncherCatalog + stable preview asset paths |
| The icon on the home screen / drawer | BeeDynamicLauncher.applyVariant(id) via one MethodChannel |
| Native projects staying in sync | dart run bee_dynamic_launcher (after one-time setup in Android / iOS) β activity aliases, Info.plist, mipmaps, asset sets |
Two layers β donβt confuse them:
| Layer | Mechanism | Use for |
|---|---|---|
| In-app | LauncherCatalog, previewIconAssetPath |
Pickers, settings, onboarding |
| OS launcher | initialize β applyVariant |
What users see outside your app |
Persistence of the userβs choice is not built in β store variantId (e.g. shared_preferences) and call applyVariant after initialize when you need to restore.
Dependencies: at runtime, flutter + this package only. The CLIβs image tooling runs on your machine when you execute dart run; it is not shipped as extra runtime weight in release builds.
β¨ Platform support
| Android | iOS | |
|---|---|---|
| Switch home-screen icon | β | β |
| JSON catalog + in-app previews | β | β |
Variant ids from Dart (initialize) β no native hard-coded list |
β | β |
| CLI: validate, manifest/plist, resize | β | β |
| Remember last choice | Your app | Your app |
Method channel: dev.bee.bee_dynamic_launcher/launcher β use this packageβs registration only; do not add a duplicate channel for the same feature.
π Requirements
- Flutter 3.24+ Β· Dart 3.5+
- Android: embedding v2
- iOS: 12.0+ (
podspec)
π Getting started
1 Β· Install
dependencies:
bee_dynamic_launcher: ^1.0.0
flutter:
assets:
- assets/bee_dynamic_launcher/catalog.json
- assets/bee_dynamic_launcher/icons/
flutter pub get
2 Β· Catalog & files
| Path | Role |
|---|---|
assets/bee_dynamic_launcher/catalog.json |
id, displayName, launcherLabel; optional primaryVariantId (map form) |
assets/bee_dynamic_launcher/icons/ic_<id>.png |
Master PNG: UI previews + CLI source for mipmaps / Xcode icons |
{
"primaryVariantId": "brand_a",
"variants": [
{ "id": "brand_a", "displayName": "Brand A", "launcherLabel": "My App" },
{ "id": "brand_b", "displayName": "Brand B", "launcherLabel": "My App Alt" }
]
}
primaryVariantId must exist in variants and matches your default shipped icon.
3 Β· Initialize native side
Call before getAvailableVariants, getCurrentVariant, or applyVariant.
Recommended:
import 'package:bee_dynamic_launcher/bee_dynamic_launcher.dart';
Future<void> setupLauncher() async {
await BeeDynamicLauncher.initializeFromCatalog();
}
Manual:
await LauncherCatalog.instance.loadFromBundle();
final cat = LauncherCatalog.instance;
await BeeDynamicLauncher.initialize(
variantIds: cat.allIds,
primaryVariantId: cat.primaryVariantId,
);
initialize registers variant ids and the primary id with Android and iOS (used for default icon naming). You do not maintain a second copy of the id list in native code beyond what the CLI generates for manifests and assets.
4 Β· In-app picker (previews)
import 'package:flutter/material.dart';
import 'package:bee_dynamic_launcher/bee_dynamic_launcher.dart';
Widget buildPickerGrid() {
final catalog = LauncherCatalog.instance;
return GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemCount: catalog.variants.length,
itemBuilder: (context, i) {
final e = catalog.variants[i];
return Column(
children: [
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.asset(e.previewIconAssetPath, fit: BoxFit.cover),
),
),
const SizedBox(height: 8),
Text(e.displayName, maxLines: 1, overflow: TextOverflow.ellipsis),
],
);
},
);
}
Catalog helpers: hasVariants, variantCount, allIds, allPreviewIconAssetPaths, containsVariant, variantEntryFor, displayNameFor, launcherLabelFor.
5 Β· Apply system launcher icon
await BeeDynamicLauncher.applyVariant('brand_b');
final String? active = await BeeDynamicLauncher.getCurrentVariant();
final List<String> ids = await BeeDynamicLauncher.getAvailableVariants();
6 Β· Full flow (StatefulWidget)
import 'package:bee_dynamic_launcher/bee_dynamic_launcher.dart';
import 'package:flutter/material.dart';
class LauncherSettingsPage extends StatefulWidget {
const LauncherSettingsPage({super.key});
@override
State<LauncherSettingsPage> createState() => _LauncherSettingsPageState();
}
class _LauncherSettingsPageState extends State<LauncherSettingsPage> {
bool _ready = false;
String? _activeId;
@override
void initState() {
super.initState();
_boot();
}
Future<void> _boot() async {
await BeeDynamicLauncher.initializeFromCatalog();
final cur = await BeeDynamicLauncher.getCurrentVariant();
if (!mounted) return;
setState(() {
_ready = true;
_activeId = cur;
});
}
Future<void> _select(String id) async {
await BeeDynamicLauncher.applyVariant(id);
final again = await BeeDynamicLauncher.getCurrentVariant();
if (!mounted) return;
setState(() => _activeId = again ?? id);
}
@override
Widget build(BuildContext context) {
if (!_ready) return const Center(child: CircularProgressIndicator());
final cat = LauncherCatalog.instance;
return ListView(
children: [
for (final e in cat.variants)
ListTile(
leading: Image.asset(e.previewIconAssetPath, width: 48, height: 48),
title: Text(e.displayName),
subtitle: Text(e.id),
trailing: _activeId == e.id ? const Icon(Icons.check) : null,
onTap: () => _select(e.id),
),
],
);
}
}
Restore a saved id after cold start: read storage β initialize β if saved != await getCurrentVariant() then applyVariant(saved).
π§° CLI (native sync)
The CLI ships with this package. After you add bee_dynamic_launcher under dependencies (see Getting started), run it from your Flutter app root β the same folder as pubspec.yaml and assets/bee_dynamic_launcher/:
dart run bee_dynamic_launcher [flags]
One-time native setup: Commands that modify AndroidManifest.xml, Info.plist, or launcher_strings_generated.xml require the sentinel comments described under Android and iOS. --scan only reads your catalog and icon files. --icons-only only regenerates image outputs (no manifest or plist text changes).
| Invocation | What it does |
|---|---|
| (no arguments) | Validate assets β Android + iOS codegen β mipmaps + iOS icons |
--help or -h |
Print usage; no file changes |
--scan |
List variant ids from catalog.json vs icons/ic_*.png; no file changes |
--scan --strict |
Like --scan, but fails if the two sets differ |
--check-android-manifest |
Check android/app/src/main/AndroidManifest.xml launcher config; no writes |
--check-ios-pbxproj |
Check ios/Runner.xcodeproj/project.pbxproj for alternate icon build setting; no writes |
--native-only or --skip-icons |
Codegen strings + manifest alias block + Info.plist alternates; skips image resize |
--icons-only |
Android mipmaps + iOS alternate icon PNGs; skips manifest / plist / strings codegen |
--wizard |
Interactively append variants to catalog.json (stdin); add ic_<id>.png then run again |
--icons-only + --native-only |
Error (use one mode at a time) |
Examples
dart run bee_dynamic_launcher --help
dart run bee_dynamic_launcher --scan
dart run bee_dynamic_launcher --scan --strict
dart run bee_dynamic_launcher --check-android-manifest
dart run bee_dynamic_launcher --check-ios-pbxproj
dart run bee_dynamic_launcher
dart run bee_dynamic_launcher --native-only
dart run bee_dynamic_launcher --skip-icons
dart run bee_dynamic_launcher --icons-only
dart run bee_dynamic_launcher --wizard
π€ Android
Where to edit
| File (your Flutter app) | android/app/src/main/AndroidManifest.xml |
| Scope | Inside the <application> element β same file where <activity android:name=".MainActivity" β¦> is declared. |
What you add manually (once)
You only add two HTML comments as start/end sentinels. The CLI will replace everything between them with generated <activity-alias> blocks on each run.
Put the pair after your main Activity (MainActivity), still inside <application>:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="β¦"
android:icon="β¦"
β¦>
<activity
android:name=".MainActivity"
β¦>
</activity>
<!-- LAUNCHER_ACTIVITY_ALIASES_BEGIN -->
<!-- LAUNCHER_ACTIVITY_ALIASES_END -->
</application>
</manifest>
- First time: the region between
BEGINandENDcan be empty (only a newline) β then rundart run bee_dynamic_launcherfrom the app project root (wherepubspec.yamllives). - After that: do not delete the two comment lines; when your catalog changes, run the CLI again so the middle is regenerated.
- Aliases use
android:targetActivity=".MainActivity"β yourMainActivitymust stay the real Flutter entry. Many white-label setups move the MAIN/LAUNCHERintent-filterfromMainActivityonto one enabled alias only; align with what the codegen outputs forprimaryVariantId.
What the CLI updates (same app module)
- Region between the markers (
activity-aliaslist). res/values/launcher_strings_generated.xml- Legacy mipmaps under
res/mipmap-*/ - Adaptive icon XML under
res/mipmap-anydpi-v26/and foreground layers underres/mipmap-*/
At runtime, variant ids come from Dart (BeeDynamicLauncher.initialize); you do not edit a separate hand-maintained list in Kotlin for each variant id.
Quick validation only (no file modification):
dart run bee_dynamic_launcher --check-android-manifest
π iOS
Where to edit
| File | ios/Runner/Info.plist |
| Scope | Under the root <dict> β you need UIApplicationSupportsAlternateIcons, and a CFBundleIcons β CFBundleAlternateIcons dictionary with the markers inside CFBundleAlternateIcons. |
Exact placement map (Info.plist)
plist
βββ dict (root)
βββ UIApplicationSupportsAlternateIcons = true
βββ CFBundleIcons (dict)
βββ CFBundlePrimaryIcon (dict) # usually already exists
βββ CFBundleAlternateIcons (dict) # add this if missing
βββ <!-- LAUNCHER_CFBundleAlternateIcons_BEGIN -->
βββ <!-- LAUNCHER_CFBundleAlternateIcons_END -->
Markers must be direct children of CFBundleAlternateIcons (not at root level, not outside CFBundleIcons).
What you add manually (once)
UIApplicationSupportsAlternateIconsmust betrue(if missing, add it next to your other top-level keys).- Inside
CFBundleIcons, ensure there is aCFBundleAlternateIconskey whose value is a<dict>. Put the BEGIN/END comments inside that inner dict β the CLI replaces only that region.
Minimal skeleton (if you have no alternate icons yet):
<key>UIApplicationSupportsAlternateIcons</key>
<true/>
<key>CFBundleIcons</key>
<dict>
<key>CFBundleAlternateIcons</key>
<dict>
<!-- LAUNCHER_CFBundleAlternateIcons_BEGIN -->
<!-- LAUNCHER_CFBundleAlternateIcons_END -->
</dict>
</dict>
If CFBundleIcons already exists (e.g. Xcode added CFBundlePrimaryIcon for AppIcon), add CFBundleAlternateIcons as a sibling key next to CFBundlePrimaryIcon β do not put the markers outside CFBundleAlternateIcons.
<key>CFBundleIcons</key>
<dict>
<key>CFBundlePrimaryIcon</key>
<dict>
β¦
</dict>
<key>CFBundleAlternateIcons</key>
<dict>
<!-- LAUNCHER_CFBundleAlternateIcons_BEGIN -->
<!-- LAUNCHER_CFBundleAlternateIcons_END -->
</dict>
</dict>
- First run: empty region between
BEGIN/ENDis OK; thendart run bee_dynamic_launcherfills alternate icon entries to match your catalog. - Primary icon set stays in
Assets.xcassets(e.g.AppIcon); alternates are generated beside it by the CLI.
Xcode build setting (required)
Alternate icon sets must be included in the compiled asset catalog. In Runner.xcodeproj, each Runner build configuration needs:
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES
The CLI enforces this automatically when you run:
dart run bee_dynamic_launcher --native-only
Quick validation only (no file modification):
dart run bee_dynamic_launcher --check-ios-pbxproj
If your app uses a custom iOS target/project layout and the CLI cannot patch project.pbxproj, set this value manually in Xcode for Debug, Release, and Profile.
Manual position in Xcode UI
- Open
ios/Runner.xcworkspacein Xcode. - Select project navigator item
Runner(blue icon). - Select target
Runner(under TARGETS, not PROJECT). - Open Build Settings tab.
- Search for
Include All App Icon Assets. - Set
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETStoYESfor:DebugReleaseProfile
If you edit raw project.pbxproj, place this key in the same buildSettings block as ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;.
Plugin note
Do not register a second launcher MethodChannel in AppDelegate β this package registers it.
π API reference
BeeDynamicLauncher
| Method | Role |
|---|---|
initialize({ variantIds, primaryVariantId }) |
Register ids + primary on native |
initializeFromCatalog() |
loadFromBundle + initialize (default paths) |
getAvailableVariants() |
Ids known to native (fallback: catalog) |
getCurrentVariant() |
OS-reported id, if any |
applyVariant(id) |
Set launcher icon |
previewIconAssetPath(id) |
Asset path for Image.asset |
LauncherCatalog.instance
| API | Role |
|---|---|
loadFromBundle |
Parse JSON from assets |
variants |
LauncherVariantEntry list |
allIds Β· primaryVariantId |
Shortcuts |
allPreviewIconAssetPaths |
All preview paths |
hasVariants Β· variantCount |
Guards |
containsVariant Β· variantEntryFor |
Lookup |
displayNameFor Β· launcherLabelFor |
Labels |
π§ Troubleshooting
| Symptom | Check |
|---|---|
applyVariant fails / no effect |
Markers + CLI; native resources exist per id |
| Android shows duplicate app icon | MainActivity must not have MAIN/LAUNCHER when alias launcher is enabled |
Image.asset error |
pubspec lists icons/; ic_<id>.png for each id |
Empty getAvailableVariants |
Run initialize after loadFromBundle |
| iOS shows placeholder icon | ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES for Runner |
| Wrong iOS icon | primaryVariantId matches default set; alternates follow CLI naming |
π License
This project is licensed under the MIT License.
π€ Contributing
For contribution flow and quality checks, see CONTRIBUTING.md. Report issues and propose changes on GitHub.
π Powered by Orion B Project
"Making coding feel like magic."



