You’ve probably come across those apps that can pull off a neat trick — changing their app icon, perhaps at the festival Or when they’re celebrating something special, and then seamlessly switching back to the regular one. Let’s change the dynamic app launcher icons in Flutter.
It’s the kind of feature that piques your curiosity, making you wonder, ‘How on earth do they do that?’ Well, you’re not alone in your curiosity.
So in this article, we’re going to unravel the mystery behind changing the dynamic app launcher icons in Flutter at runtime. We’ll break it down for you, step by step, and show you that it’s not just doable, but also pretty manageable.
Additionally, you may read more about Mobikul’s Flutter app development services.
We’ll break this into two parts:
- First For the Android
- Second For the iOS
Note: We’re going to use the Method Channel for this Process. You can also check the Blog for the Method Channel.
Android Part
Here is a simple way to change your Android app’s Launcher icon and Application name dynamically.
Step 1
Create an empty class each for the number of icons wanted to declare. In my example, I want to create 3 launcher icon which I would be changing it dynamically. So I created FirstLauncherAlias.kt, SecondLauncherAlias.kt and ThirdLauncherAlias.kt
1 2 3 4 5 6 |
package com.example.fl_chart_blog.launcherAlias class FirstLauncherAlias { } |
Step 2
Add activity-alias in your AndroidManifest.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
<activity-alias android:name=".launcherAlias.FirstLauncherAlias" android:enabled="false" android:icon="@mipmap/ic_launcher_one" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_one_round" android:targetActivity=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <!-- TDOD: Add Your Deep Link Code here --> <data android:host="host_name" android:pathPrefix="prefix_key: eg /" android:scheme="https or http" /> </intent-filter> </activity-alias> |
NOTE: Generate the different launcher icons for the alias.
This we’ll be our complete AndroidManifest.xml File.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <uses-permission android:name="android.permission.INTERNET" /> <application android:name="${applicationName}" android:icon="@mipmap/ic_launcher_one" android:label="@string/app_name"> <activity android:name=".MainActivity" android:configChanges="orientation|keyboardHidden|keyboard |screenSize|smallestScreenSize|locale|layoutDirection |fontScale|screenLayout|density|uiMode" android:exported="true" android:enabled="false" android:hardwareAccelerated="true" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:windowSoftInputMode="adjustResize"> <!-- Specifies an Android theme to apply to this Activity as soon as the Android process has started. This theme is visible to the user while the Flutter UI initializes. After that, this theme continues to determine the Window background behind the Flutter UI. --> <meta-data android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/NormalTheme" /> <intent-filter> <action android:name="android.intent.action.MAIN" /> <!-- <category android:name="android.intent.category.LAUNCHER" />--> </intent-filter> </activity> <activity-alias android:name=".launcherAlias.SecondLauncherAlias" android:enabled="false" android:icon="@mipmap/ic_launcher_two" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_two_round" android:targetActivity=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:host="host_name" android:pathPrefix="prefix_key: eg /" android:scheme="https or http" /> </intent-filter> </activity-alias> <activity-alias android:name=".launcherAlias.FirstLauncherAlias" android:enabled="false" android:icon="@mipmap/ic_launcher_one" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_one_round" android:targetActivity=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <!-- TDOD: Add Your Deep Link Code here --> <data android:host="host_name" android:pathPrefix="prefix_key: eg /" android:scheme="https or http" /> </intent-filter> </activity-alias> <activity-alias android:name=".launcherAlias.DefaultLauncherAlias" android:enabled="true" android:icon="@mipmap/ic_launcher_default" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_default_round" android:targetActivity=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:host="host_name" android:pathPrefix="prefix_key: eg /" android:scheme="https or http" /> </intent-filter> </activity-alias> <!-- Don't delete the meta-data below. This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> <meta-data android:name="flutterEmbedding" android:value="2" /> </application> </manifest> |
Step 3
Enable and disable the activity alias based on your requirements. In my example, On clicking on one layout, I am enabling the activity-alias FirstLauncherAlias and disabling the rest.
This we’ll be our complete MainActivity.kt File.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
package com.example.fl_chart_blog import android.content.BroadcastReceiver import android.content.ComponentName import android.content.pm.PackageManager import androidx.annotation.NonNull import com.example.fl_chart_blog.helper.AppSharedPref import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel class MainActivity : FlutterActivity() { private val CHANNEL = "com.dynamicIcon" var methodChannelResult: MethodChannel.Result? = null @Override override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) MethodChannel(flutterEngine.dartExecutor, CHANNEL).setMethodCallHandler { call, result -> try { methodChannelResult = result; if (call.method.equals("launcherFirst")) { AppSharedPref.setLauncherIcon(this, "launcherAlias.FirstLauncherAlias") AppSharedPref.setCount(this, 1) result.success(true) } else if (call.method.equals("launcherSecond")) { AppSharedPref.setLauncherIcon(this, "launcherAlias.SecondLauncherAlias") AppSharedPref.setCount(this, 2) result.success(true) } else { AppSharedPref.setLauncherIcon(this, "launcherAlias.DefaultLauncherAlias") AppSharedPref.setCount(this, 0) result.success(true) } } catch (e: Exception) { print(e) } } } // App icon will change at the time when your activity gets destroyed, To Prevent the unnecessary app close issue. override fun onDestroy() { super.onDestroy() setIcon(AppSharedPref.getLauncherIcon(this).toString(), AppSharedPref.getCount(this)) } //dynamically change app icon private fun setIcon(targetIcon: String, index: Int) { try { val packageManager: PackageManager = applicationContext!!.packageManager val packageName = applicationContext!!.packageName val className = StringBuilder() className.append(packageName) className.append(".") className.append(targetIcon) when (index) { 0 -> { packageManager.setComponentEnabledSetting( ComponentName(packageName, "$packageName.$targetIcon"), PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP ) packageManager.setComponentEnabledSetting( ComponentName( packageName!!, "$packageName.launcherAlias.FirstLauncherAlias" ), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP ) packageManager.setComponentEnabledSetting( ComponentName( packageName, "$packageName.launcherAlias.SecondLauncherAlias" ), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP ) } 1 -> { packageManager.setComponentEnabledSetting( ComponentName(packageName, "$packageName.$targetIcon"), PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP ) packageManager.setComponentEnabledSetting( ComponentName( packageName!!, "$packageName.launcherAlias.DefaultLauncherAlias" ), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP ) packageManager.setComponentEnabledSetting( ComponentName( packageName!!, "$packageName.launcherAlias.SecondLauncherAlias" ), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP ) } 2 -> { packageManager.setComponentEnabledSetting( ComponentName(packageName, "$packageName.$targetIcon"), PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP ) packageManager.setComponentEnabledSetting( ComponentName( packageName!!, "$packageName.launcherAlias.DefaultLauncherAlias" ), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP ) packageManager.setComponentEnabledSetting( ComponentName( packageName!!, "$packageName.launcherAlias.FirstLauncherAlias" ), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP ) } } } catch (e: Exception) { print(e) } } } |
Step 4
This we’ll be our complete AppSharedPref.kt File.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
package com.example.fl_chart_blog.helper import android.content.Context import android.content.Context.MODE_PRIVATE import android.content.SharedPreferences open class AppSharedPref { companion object { const val CONFIGURATION_PREF = "configurationPreference" /*launcher icon*/ private const val KEY_LAUNCHER_IMAGE = "launcherIcon" private const val KEY_LAUNCHER_COUNT = "count" private const val KEY_LAUNCHER_SAVED_COUNT = "savedCount" fun getSharedPreference(context: Context, preferenceFile: String): SharedPreferences { return context.getSharedPreferences(preferenceFile, MODE_PRIVATE) } fun getSharedPreferenceEditor( context: Context, preferenceFile: String ): SharedPreferences.Editor { return context.getSharedPreferences(preferenceFile, MODE_PRIVATE).edit() } /* Settings Related functions */ fun getLauncherIcon(context: Context): String? { return getSharedPreference(context, CONFIGURATION_PREF).getString( KEY_LAUNCHER_IMAGE, "launcherAlias.DefaultLauncherAlias" ) } fun setLauncherIcon(context: Context, launcherIcon: String) { getSharedPreferenceEditor(context, CONFIGURATION_PREF).putString( KEY_LAUNCHER_IMAGE, launcherIcon ).apply() } fun getCount(context: Context): Int { return getSharedPreference(context, CONFIGURATION_PREF).getInt( KEY_LAUNCHER_COUNT, 0 ) } fun setCount(context: Context, count: Int) { getSharedPreferenceEditor(context, CONFIGURATION_PREF).putInt( KEY_LAUNCHER_COUNT, count ).apply() } fun getSavedCount(context: Context): Int { return getSharedPreference(context, CONFIGURATION_PREF).getInt( KEY_LAUNCHER_SAVED_COUNT, 0 ) } fun setSavedCount(context: Context, count: Int) { getSharedPreferenceEditor(context, CONFIGURATION_PREF).putInt( KEY_LAUNCHER_SAVED_COUNT, count ).apply() } } } |
iOS Part
In iOS 10.3 version, Apple introduced an amazing feature using which developers can now change the app’s icon with predefined additional icons through code, without re-submitting the app to the App store.
Step 1
Create alternate app icons of specific sizes
We just need two sizes of our alternate icon
- DefaultAppIcon@2x.png = 120 x 120 pixels
- DefaultAppIcon@3x.png = 180 x 180 pixels
We need to add @2x and @3x after our icon name for the system to know which one to use on different screens.
We are supposed to add these icons inside our project folder and not in the Assets.xcassets, as we do with other icons/images.
Step 2
Now we have to add the alternate icon in our Info.plist file.
- Add Icon files (iOS 5)/CFBundleIcons to the Info.plist
- Add CFBundleAlternateIcons as a Dictionary which will be used for alternative icons
For each alternate icon, add a new entry in the info.plist file, under CFBundleAlternateIcons.
Here we have kept the same name for both for easy to use & simplicity.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>CFBundleIcons</key> <dict> <key>CFBundleAlternateIcons</key> <dict> <key>AppIconDefault</key> <dict> <key>UIPrerenderedIcon</key> <false/> <key>CFBundleIconFiles</key> <array> <string>AppIconDefault</string> </array> </dict> <key>AppIconOne</key> <dict> <key>UIPrerenderedIcon</key> <false/> <key>CFBundleIconFiles</key> <array> <string>AppIconOne</string> </array> </dict> <key>AppIconTwo</key> <dict> <key>UIPrerenderedIcon</key> <false/> <key>CFBundleIconFiles</key> <array> <string>AppIconTwo</string> </array> </dict> <key>AppIconThree</key> <dict> <key>UIPrerenderedIcon</key> <false/> <key>CFBundleIconFiles</key> <array> <string>AppIconThree</string> </array> </dict> </dict> <key>CFBundlePrimaryIcon</key> <dict> <key>CFBundleIconName</key> <string></string> <key>CFBundleIconFiles</key> <array> <string>AppIconDefault</string> </array> <key>UIPrerenderedIcon</key> <false/> </dict> <key>UINewsstandIcon</key> <dict> <key>CFBundleIconFiles</key> <array> <string></string> </array> <key>UINewsstandBindingType</key> <string>UINewsstandBindingTypeMagazine</string> <key>UINewsstandBindingEdge</key> <string>UINewsstandBindingEdgeLeft</string> </dict> </dict> <key>CFBundleDevelopmentRegion</key> <string>$(DEVELOPMENT_LANGUAGE)</string> <key>CFBundleDisplayName</key> <string>Fl Chart Blog</string> <key>CFBundleExecutable</key> <string>$(EXECUTABLE_NAME)</string> <key>CFBundleIdentifier</key> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> <key>CFBundleInfoDictionaryVersion</key> <string>6.0</string> <key>CFBundleName</key> <string>fl_chart_blog</string> <key>CFBundlePackageType</key> <string>APPL</string> <key>CFBundleShortVersionString</key> <string>$(FLUTTER_BUILD_NAME)</string> <key>CFBundleSignature</key> <string>????</string> <key>CFBundleVersion</key> <string>$(FLUTTER_BUILD_NUMBER)</string> <key>LSRequiresIPhoneOS</key> <true/> <key>UILaunchStoryboardName</key> <string>LaunchScreen</string> <key>UIMainStoryboardFile</key> <string>Main</string> <key>UISupportedInterfaceOrientations</key> <array> <string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeRight</string> </array> <key>UISupportedInterfaceOrientations~ipad</key> <array> <string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortraitUpsideDown</string> <string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeRight</string> </array> <key>UIViewControllerBasedStatusBarAppearance</key> <false/> <key>CADisableMinimumFrameDurationOnPhone</key> <true/> <key>UIApplicationSupportsIndirectInputEvents</key> <true/> </dict> </plist> |
Step 3
Used this piece of code to update the app icon in the iOS app.
1 2 3 4 5 6 7 8 9 |
func setIcon(_ appIcon: String) { if UIApplication.shared.supportsAlternateIcons{ UIApplication.shared.setAlternateIconName(appIcon) { error in if let error = error { print("Error setting alternate icon \(appIcon): \(error.localizedDescription)") } } } } |
This will be our complete AppDelegate file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
import UIKit import Flutter import Foundation @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { let controller : FlutterViewController = window?.rootViewController as! FlutterViewController let mlkitChannel = FlutterMethodChannel(name: "com.dynamicIcon", binaryMessenger: controller.binaryMessenger) mlkitChannel.setMethodCallHandler({ (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in if call.method == "launcherFirst"{ /* Start */ if #available(iOS 10.3, *) { let iconName = "AppIconOne"; self.setIcon(iconName) }else { result("Not supported on iOS ver < 10.3"); } /* End */ }else if call.method == "launcherSecond"{ /* Start */ if #available(iOS 10.3, *) { let iconName = "AppIconTwo"; self.setIcon(iconName) }else { result("Not supported on iOS ver < 10.3"); } /* End */ }else if call.method == "default"{ /* Start */ if #available(iOS 10.3, *) { let iconName = "AppIconDefault"; self.setIcon(iconName) }else { result("Not supported on iOS ver < 10.3"); } /* End */ } }) GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } func setIcon(_ appIcon: String) { if UIApplication.shared.supportsAlternateIcons{ UIApplication.shared.setAlternateIconName(appIcon) { error in if let error = error { print("Error setting alternate icon \(appIcon): \(error.localizedDescription)") } } } } } |
Let’s Move on to the Flutter side code.
This will be our dynamic_launcher.dart file code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; class DynamicLauncher extends StatefulWidget { const DynamicLauncher({Key? key}) : super(key: key); @override State<DynamicLauncher> createState() => _DynamicLauncherState(); } class _DynamicLauncherState extends State<DynamicLauncher> { static const platform = MethodChannel('com.dynamicIcon'); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: const Text('Dynamic Launcher Icons'), ), body: Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ const Text('Change Launcher Icon'), const SizedBox(height: 50), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ TextButton.icon( onPressed: () async { await platform.invokeMethod('launcherFirst'); }, icon: const Icon( Icons.android, color: Colors.red, ), label: const Text( 'One', style: TextStyle(color: Colors.red), ), ), TextButton.icon( onPressed: () async { await platform.invokeMethod('launcherSecond'); }, icon: const Icon( Icons.android, color: Colors.blue, ), label: const Text( 'Two', style: TextStyle(color: Colors.blue), ), ), TextButton.icon( onPressed: () async { await platform.invokeMethod('default'); }, icon: const Icon( Icons.restore, color: Colors.black, ), label: const Text( 'Restore', style: TextStyle(color: Colors.black), ), ), ], ), ], ), ); } } |
Here is the final output for the Android App.
Dynamic App Launcher Icons
Conclusion
Now you also know how to change Dynamic App Launcher Icons In Flutter.
Thanks for reading this blog. You can also check other blogs from here for more knowledge.
Always be ready for learning