Building a Dynamic App Icon with Kotlin: A Guide for Android Developers
Introduction
In this article, we will walk through the process of creating a simple yet effective feature for your Android app: setting a dynamic app icon using emojis without the need to restart the app. This feature can be a fun way to personalise the user experience and can be easily implemented in a Kotlin-based Android application.
Let’s deep dive into the implementation
Android Manifest
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.DynamicAppIcon"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.DynamicAppIcon">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity-alias
android:name=".MainActivityAngry"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_angry"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_angry"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<activity-alias
android:name=".MainActivityCool"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_cool"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_cool"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<activity-alias
android:name=".MainActivityCry"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_cry"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_cry"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<activity-alias
android:name=".MainActivityDevilish"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_devilish"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_devilish"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<activity-alias
android:name=".MainActivityDizzy"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_dizzy"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_dizzy"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<activity-alias
android:name=".MainActivityHappy"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_happy"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_happy"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<activity-alias
android:name=".MainActivityIll"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_ill"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_ill"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<activity-alias
android:name=".MainActivityInjured"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_injured"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_injured"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<activity-alias
android:name=".MainActivityLaughing"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_laughing"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_laughing"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<activity-alias
android:name=".MainActivityLove"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_love"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_love"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<activity-alias
android:name=".MainActivitySad"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_sad"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_sad"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<activity-alias
android:name=".MainActivitySleepy"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_sleepy"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_sleepy"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
</application>
</manifest>
Main Activity
package com.ovais.dynamicappicon
import android.content.ComponentName
import android.content.pm.PackageManager
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.ovais.dynamicappicon.ui.theme.DynamicAppIconTheme
class MainActivity : ComponentActivity() {
private val mainViewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
DynamicAppIconTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
val items by mainViewModel.appIcons.collectAsState()
MoodScreen(
innerPadding,
items
) { icon ->
onAppIconSelection(icon)
}
}
}
}
}
private fun onAppIconSelection(icon: AppIcon) {
val context = baseContext
val packageManager = context.packageManager
mainViewModel.appIcons.value.filterNot { it.id == icon.id }.forEach {
packageManager.setComponentEnabledSetting(
ComponentName(context, it.componentName),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP
)
}
packageManager.setComponentEnabledSetting(
ComponentName(context, icon.componentName),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP
)
Toast.makeText(this, "App icon changed to ${icon.id}", Toast.LENGTH_SHORT).show()
}
}
Main ViewModel
class MainViewModel : ViewModel() {
private val _appIcons by lazy {
MutableStateFlow(
arrayListOf(
AppIcon(
id = "angry",
resId = R.mipmap.ic_angry,
componentName = buildComponentName("angry")
),
AppIcon(
id = "cool",
resId = R.mipmap.ic_cool,
componentName = buildComponentName("cool")
),
AppIcon(
id = "cry",
resId = R.mipmap.ic_cry,
componentName = buildComponentName("cry")
),
AppIcon(
id = "devilish",
resId = R.mipmap.ic_devilish,
componentName = buildComponentName("devilish")
),
AppIcon(
id = "happy",
resId = R.mipmap.ic_happy,
componentName = buildComponentName("happy")
),
AppIcon(
id = "ill",
resId = R.mipmap.ic_ill,
componentName = buildComponentName("ill")
),
AppIcon(
id = "injured",
resId = R.mipmap.ic_injured,
componentName = buildComponentName("injured")
),
AppIcon(
id = "laughing",
resId = R.mipmap.ic_laughing,
componentName = buildComponentName("laughing")
),
AppIcon(
id = "love",
resId = R.mipmap.ic_love,
componentName = buildComponentName("love")
),
AppIcon(
id = "sad",
resId = R.mipmap.ic_sad,
componentName = buildComponentName("sad")
),
AppIcon(
id = "sleepy",
resId = R.mipmap.ic_sleepy,
componentName = buildComponentName("sleepy")
),
)
)
}
val appIcons: StateFlow<ArrayList<AppIcon>>
get() = _appIcons
private fun buildComponentName(name: String): String {
return "com.ovais.dynamicappicon.MainActivity${name.capitalize(Locale.current)}"
}
}
Sample UI
@Composable
fun MoodScreen(
padding: PaddingValues = PaddingValues(),
items: List<AppIcon>,
onMoodSelection: (AppIcon) -> Unit
) {
Column(
modifier = Modifier
.background(AppBackground)
.padding(padding)
.fillMaxSize()
) {
LazyVerticalGrid(
columns = GridCells.Fixed(3),
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 8.dp),
verticalArrangement = Arrangement.Center,
horizontalArrangement = Arrangement.Center
) {
items(items) {
MoodItem(
icon = it,
onClick = onMoodSelection
)
}
}
}
}
@Composable
fun MoodItem(
icon: AppIcon,
modifier: Modifier = Modifier,
onClick: (AppIcon) -> Unit
) {
Column(
modifier = modifier.then(
Modifier
.clickable { onClick(icon) }
.border(
1.dp,
color = Color.Gray,
shape = RoundedCornerShape(12.dp)
)
.padding(
8.dp
)
)
) {
Image(
painter = painterResource(icon.resId),
contentDescription = null
)
Text(
icon.id.capitalize(Locale.current),
color = Color.White
)
}
}
@Preview
@Composable
private fun MoodItemPreview() {
MoodItem(
icon = AppIcon(
"angry",
R.mipmap.ic_angry,
"angry"
)
) {
}
}
App Icon DTO
data class AppIcon(
val id: String,
@DrawableRes val resId: Int,
val componentName: String
)
Final Demo
https://youtube.com/shorts/BOUDlN2N52E?feature=share
Conclusion
In this guide, we have demonstrated how to implement a dynamic app icon feature in an Android app using Kotlin. By leveraging activity-alias
and Kotlin's PackageManager
, we were able to allow users to switch between various icons, enhancing personalization without restarting the app. This approach provides a fun and engaging way to adapt the app's appearance based on user preferences, such as changing the icon to reflect different moods or emotions.
We explored each step of the implementation, from configuring the Android Manifest file to building the user interface with Jetpack Compose. The result is a clean, dynamic icon feature that elevates the user experience and adds a touch of interactivity.
By following this approach, developers can not only add a unique visual twist to their app but also improve user engagement. The flexibility of Kotlin and Android’s package management tools allows you to easily extend this functionality with additional icon options or more complex dynamic features in the future. With this guide, you’re well on your way to making your app stand out with dynamic app icons that users will enjoy customizing!