Reputation: 396
Is there a way to dynamically set an icon value in Jetpack Compose?
Example, instead of:
Icon(Icons.Filled.Print, "print")
I'd like to do:
Icon(Icons.Filled.(iconValue), iconValueName)
Upvotes: 1
Views: 2057
Reputation: 13826
Official Google code for this problem; note this is an internal function so add it to your utils:
/**
* Utility delegate to construct a Material icon with default size information.
* This is used by generated icons, and should not be used manually.
*
* @param name the full name of the generated icon
* @param autoMirror determines if the vector asset should automatically be mirrored for right to
* left locales
* @param block builder lambda to add paths to this vector asset
*/
inline fun materialIcon(
name: String,
autoMirror: Boolean = false,
block: ImageVector.Builder.() -> ImageVector.Builder
): ImageVector = ImageVector.Builder(
name = name,
defaultWidth = MaterialIconDimension.dp,
defaultHeight = MaterialIconDimension.dp,
viewportWidth = MaterialIconDimension,
viewportHeight = MaterialIconDimension,
autoMirror = autoMirror
).block().build()
Upvotes: 0
Reputation: 384
Based on @phil-dukhov answer, here's a generalized implementation:
// File MaterialIconUtils.kt
package your.utils.package.goes.here
import android.util.Log
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BugReport
import androidx.compose.ui.graphics.vector.ImageVector
/** Regex to split icon name. */
private val splitter = Regex("\\.")
/** Prefix for Material icons package. */
private val iconsPackage = "androidx.compose.material."
/** Decodes a Material Icon using its "compose" name.
*
* For example:
* - Icon `Icons.AutoMirrored.TwoTone.ArrowBack`
* - Looks for class `androidx.compose.material.icons.filled.ArrowBackKt`
* - Attempts to find method `getArrowBack(androidx.compose.material.icons.Icons$AutoMirrored$TwoTone)`
* - Executes such method using static instance `Icons.AutoMirrored.TwoTone`
*
* Remember that Material Icons are organized as follows:
* ```
* Icons.Default.<iconName> --> androidx.compose.material.icons.filled
* Icons.Filled.<iconName> --> androidx.compose.material.icons.filled
* Icons.Rounded.<iconName> --> androidx.compose.material.icons.rounded
* Icons.Outlined.<iconName> --> androidx.compose.material.icons.outlined
* Icons.TwoTone.<iconName> --> androidx.compose.material.icons.twotone
* Icons.Sharp.<iconName> --> androidx.compose.material.icons.sharp
* Icons.AutoMirrored.Default.<iconName> --> androidx.compose.material.icons.automirrored.filled
* Icons.AutoMirrored.Filled.<iconName> --> androidx.compose.material.icons.automirrored.filled
* Icons.AutoMirrored.Rounded.<iconName> --> androidx.compose.material.icons.automirrored.rounded
* Icons.AutoMirrored.Outlined.<iconName> --> androidx.compose.material.icons.automirrored.outlined
* Icons.AutoMirrored.Rounded.<iconName> --> androidx.compose.material.icons.automirrored.rounded
* Icons.AutoMirrored.TwoTone.<iconName> --> androidx.compose.material.icons.automirrored.twotone
* Icons.AutoMirrored.Sharp.<iconName> --> androidx.compose.material.icons.automirrored.sharp
* ```
*
* Also, don't forget to include in gradle:
* ```
* platform("androidx.compose:compose-bom:2024.02.01").also { compose ->
* implementation(compose)
* androidTestImplementation(compose)
* :
* implementation("androidx.compose.material:material-icons-core")
* implementation("androidx.compose.material:material-icons-extended")
* :
* }
* ```
*
* @param name Icon name.
* @param default Default icon if [name] is invalid.
*/
fun decodeMaterialIcon(
name: String,
default: ImageVector? = Icons.Default.BugReport
): ImageVector? =
try {
val parts = splitter.split(name)
val className: String
val typeName: String
val iconName: String
when (parts.size) {
3 -> {
className = buildString {
append(iconsPackage)
append(parts[0].lowercase())
append('.')
val type = parts[1].lowercase()
if (type == "default") {
append("filled")
} else {
append(type)
}
append('.')
append(parts[2])
append("Kt")
}
typeName = buildString {
append(parts[0])
append('.')
val type = parts[1]
if (type == "Default") {
append("Filled")
} else {
append(type)
}
}
iconName = parts[2]
}
4 -> {
className = buildString {
append(iconsPackage)
append(parts[0].lowercase())
append('.')
append(parts[1].lowercase())
append('.')
val type = parts[2].lowercase()
if (type == "default") {
append("filled")
} else {
append(type)
}
append('.')
append(parts[3])
append("Kt")
}
typeName = buildString {
append(parts[0])
append('.')
append(parts[1])
append('.')
val type = parts[2]
if (type == "Default") {
append("Filled")
} else {
append(type)
}
}
iconName = parts[3]
}
else -> throw IllegalArgumentException("Invalid icon-name '$name'")
}
val typeClass: Any = when (typeName) {
"Icons.Filled" -> Icons.Filled
"Icons.Outlined" -> Icons.Outlined
"Icons.Rounded" -> Icons.Rounded
"Icons.TwoTone" -> Icons.TwoTone
"Icons.Sharp" -> Icons.Sharp
"Icons.AutoMirrored.Filled" -> Icons.AutoMirrored.Filled
"Icons.AutoMirrored.Outlined" -> Icons.AutoMirrored.Outlined
"Icons.AutoMirrored.Rounded" -> Icons.AutoMirrored.Rounded
"Icons.AutoMirrored.TwoTone" -> Icons.AutoMirrored.TwoTone
"Icons.AutoMirrored.Sharp" -> Icons.AutoMirrored.Sharp
else -> throw IllegalArgumentException("Invalid icon-name '$name'")
}
Class.forName(className).getDeclaredMethod("get$iconName", typeClass.javaClass).invoke(
null,
typeClass
) as ImageVector
} catch (e: Throwable) {
Log.d("icons", "Icon-error: ${e.message}")
/* Note: if anything goes wrong:
* - re-throw exception, or
* - return a default icon (e.g. BugReport)
* Warning: this may introduce unexpected results, or
* - return null - composables should not render icon, or use a different mechanism.
*/
default
}
To make it easier to use in composables, change signature to:
Note that to make it easier for composable and avoid nullables, just change signature as:
fun decodeMaterialIcon(
name: String,
default: ImageVector = Icons.Default.BugReport
): ImageVector = ...
Upvotes: 0
Reputation: 276
You can then use a when statement and select appropriate image vector.
when(imageStringFromWeb) {
is "email" -> {
Icon(Icons.Filled.Email, null)
}
...
}
Edited to simplfy the code.
Upvotes: -1
Reputation: 87764
You can use Java reflection. I rely on the fact that each material icon is placed in a separate file, and all of them are declared under androidx.compose.material.icons.filled
package.
@Composable
fun IconByName(name: String) {
val icon: ImageVector? = remember(name) {
try {
val cl = Class.forName("androidx.compose.material.icons.filled.${name}Kt")
val method = cl.declaredMethods.first()
method.invoke(null, Icons.Filled) as ImageVector
} catch (_: Throwable) {
null
}
}
if (icon != null) {
Icon(icon, "$name icon")
}
}
You can check out this answer for more details how Kotlin extensions are compiled to Java code.
I would also write a test for this logic using a couple of icons, just in case Compose changes something in future releases - a package name or moving multiple icons together in the same file, although this is unlikely.
Upvotes: 6