Zim
Zim

Reputation: 396

Dynamic Icon value selector in Jetpack Compose

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

Answers (4)

A T
A T

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()

Reference: https://androidx.tech/artifacts/compose.material/material-icons-core/1.0.0-beta05-source/androidx/compose/material/icons/Icons.kt.html

Upvotes: 0

miguelt
miguelt

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

mucahid-erdogan
mucahid-erdogan

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

Phil Dukhov
Phil Dukhov

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

Related Questions