
Expose full Material Design Icons catalog as generated vector icons with compile-time safe accessors, runtime name lookup (aliases/renames), optional RTL mirroring, and bitmap export.
A Compose Multiplatform library exposing the full Material Design Icons (MDI) catalog as Kotlin, targeting Android, iOS, desktop (JVM), JS and wasm.
Browse the catalog in your browser: the sample app from this repository, running on Compose for web.
The library is published on Maven Central:
dependencies {
implementation("io.github.timoptr:mdi-icons:0.2.0")
}In a Compose Multiplatform project, add it to the commonMain source set instead:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.timoptr:mdi-icons:0.2.0")
}
}
}Every icon has a generated accessor on Mdi, named after the icon in PascalCase. Accessors are
extension properties in the io.github.timoptr.mdiicons.generated package, so the IDE imports
each one you use:
import io.github.timoptr.mdiicons.Mdi
import io.github.timoptr.mdiicons.generated.HomeAssistant
import io.github.timoptr.mdiicons.rememberImageVector
@Composable
fun HomeIcon() {
Icon(
imageVector = Mdi.HomeAssistant.rememberImageVector(),
contentDescription = "Home Assistant",
)
}The result is a regular ImageVector, so it works with Icon, Image and tinting like the
Material icons.
Names coming from a server or user settings resolve with fromMdiName, which takes the name
without the mdi: prefix. Aliases and historical renames are followed; unknown or removed names return null,
so you choose the fallback:
val icon = Mdi.fromMdiName("lightbulb-on") ?: Mdi.HelpCircle
Icon(imageVector = icon.rememberImageVector(), contentDescription = null)Icon(
imageVector = Mdi.ArrowLeft.rememberImageVector(autoMirror = true),
contentDescription = "Back",
)For an icon picker, Mdi.icons returns every icon:
LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 44.dp)) {
items(Mdi.icons, key = MdiIcon::name) { icon ->
Icon(imageVector = icon.rememberImageVector(), contentDescription = icon.name)
}
}For notifications, quick settings tiles, widgets or Android Auto, which cannot render Compose:
val bitmap = Mdi.HomeAssistant.toBitmap(context, sizeDp = 24, color = Color.WHITE)
NotificationCompat.Builder(context, channelId)
.setLargeIcon(bitmap)This library is for Compose Multiplatform (and Kotlin/Compose Android) projects: icons are
exposed as Compose ImageVectors and looked up by the icon names. It is not
aimed at other ecosystems (not using Compose Multiplatform), which have better native options for the same upstream data:
@mdi/js
from npm, tree-shakeable and idiomatic.@mdi/svg assets
directly, as the Home Assistant iOS app does with its own generated catalog.The Home Assistant Android app used Android-Iconics with the
community-material-typeface to render MDI icons. That approach reached a dead end:
This library replaces all of that with a small, owned pipeline over the canonical data.
@mdi/svg version.meta.json aliases and the historical renames.
Unknown or removed icons resolve to null so callers pick their own fallback.val per icon.ImageVectors, identical to the frontend's path-based
rendering.AutoMirrored icons. MDI carries no
per-icon RTL metadata, so mirroring is opted into per call site.toBitmap extensions for surfaces that cannot render Compose../gradlew :shared:updateMdiIcons regenerates the catalog from the version
pinned in gradle/libs.versions.toml, verified against the npm registry checksum. Renovate
watches the pin, and verifyMdiIcons fails CI until the catalog is regenerated after a bump.Shipping the catalog as Kotlin trades binary size against an updatable, multiplatform library. Measured on the Home Assistant Android app (release build), replacing Iconics and its typeface with this catalog:
| Metric | Impact | Notes |
|---|---|---|
| APK download size | +0.6 MB | The ~2.5 MB of path strings compress well; removing the 1.3 MB font offsets most of it |
| Raw dex (install size) | +4 MB | Each icon costs its path data plus roughly 800 bytes of dex structure for the accessor val
|
| RAM | pay per use | See below; the previous typeface enums held 1 to 1.5 MB of heap unconditionally |
The dex cost is the price of one accessor per icon. Icon fonts remain the most byte-efficient
encoding for mono-color icon sets (the font is memory mapped, so glyph shapes never touch the
Java heap either); this library deliberately spends those bytes on the canonical data format,
compile-time safety and ImageVector rendering.
The catalog is split into alphabetically sorted chunks, and a lookup loads only the chunk holding the requested name (binary search over the chunks' first names).
Measured retained heap on the JVM, first use from a cold catalog:
| Scenario | Retained |
|---|---|
| Single icon lookup | ~280 KiB |
| Six lookups spread across six different chunks (adversarial) | ~1.6 MiB |
Listing the full catalog through Mdi.icons (icon picker) |
~3.9 MiB |
Typical sessions resolve icons that cluster alphabetically, so real usage sits between the first two rows. The alias and rename tables load only when a lookup misses the canonical names.
ImageVectors are deliberately not cached in the library. rememberImageVector() scopes them to
the composition, so vectors exist only while their icon is on screen. A screen listing the whole
catalog pays for what it shows and releases it on dispose; nothing accumulates for the lifetime
of the process. toBitmap likewise returns a fresh bitmap on each call because its consumers
immediately parcel it to a system surface.
The library publishes to Maven Central as io.github.timoptr:mdi-icons through the
vanniktech maven publish plugin,
following the Kotlin Multiplatform publishing guide.
One publication covers every target: Android, iOS (arm64 and simulator), desktop (JVM), JS and wasm.
./gradlew :shared:updateMdiIcons # regenerate from the pinned @mdi/svg version
./gradlew :shared:verifyMdiIcons # check the generated catalog matches the pinThe version is pinned in gradle/libs.versions.toml under mdi-svg with a Renovate annotation.
The images in docs/images are Roborazzi screenshot tests of
the sample app, rendered with Robolectric:
./gradlew :sample:recordRoborazziAndroidHostTest # re-record after a sample UI change
./gradlew :sample:verifyRoborazziAndroidHostTest # check the images still match the appA Compose Multiplatform library exposing the full Material Design Icons (MDI) catalog as Kotlin, targeting Android, iOS, desktop (JVM), JS and wasm.
Browse the catalog in your browser: the sample app from this repository, running on Compose for web.
The library is published on Maven Central:
dependencies {
implementation("io.github.timoptr:mdi-icons:0.2.0")
}In a Compose Multiplatform project, add it to the commonMain source set instead:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.timoptr:mdi-icons:0.2.0")
}
}
}Every icon has a generated accessor on Mdi, named after the icon in PascalCase. Accessors are
extension properties in the io.github.timoptr.mdiicons.generated package, so the IDE imports
each one you use:
import io.github.timoptr.mdiicons.Mdi
import io.github.timoptr.mdiicons.generated.HomeAssistant
import io.github.timoptr.mdiicons.rememberImageVector
@Composable
fun HomeIcon() {
Icon(
imageVector = Mdi.HomeAssistant.rememberImageVector(),
contentDescription = "Home Assistant",
)
}The result is a regular ImageVector, so it works with Icon, Image and tinting like the
Material icons.
Names coming from a server or user settings resolve with fromMdiName, which takes the name
without the mdi: prefix. Aliases and historical renames are followed; unknown or removed names return null,
so you choose the fallback:
val icon = Mdi.fromMdiName("lightbulb-on") ?: Mdi.HelpCircle
Icon(imageVector = icon.rememberImageVector(), contentDescription = null)Icon(
imageVector = Mdi.ArrowLeft.rememberImageVector(autoMirror = true),
contentDescription = "Back",
)For an icon picker, Mdi.icons returns every icon:
LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 44.dp)) {
items(Mdi.icons, key = MdiIcon::name) { icon ->
Icon(imageVector = icon.rememberImageVector(), contentDescription = icon.name)
}
}For notifications, quick settings tiles, widgets or Android Auto, which cannot render Compose:
val bitmap = Mdi.HomeAssistant.toBitmap(context, sizeDp = 24, color = Color.WHITE)
NotificationCompat.Builder(context, channelId)
.setLargeIcon(bitmap)This library is for Compose Multiplatform (and Kotlin/Compose Android) projects: icons are
exposed as Compose ImageVectors and looked up by the icon names. It is not
aimed at other ecosystems (not using Compose Multiplatform), which have better native options for the same upstream data:
@mdi/js
from npm, tree-shakeable and idiomatic.@mdi/svg assets
directly, as the Home Assistant iOS app does with its own generated catalog.The Home Assistant Android app used Android-Iconics with the
community-material-typeface to render MDI icons. That approach reached a dead end:
This library replaces all of that with a small, owned pipeline over the canonical data.
@mdi/svg version.meta.json aliases and the historical renames.
Unknown or removed icons resolve to null so callers pick their own fallback.val per icon.ImageVectors, identical to the frontend's path-based
rendering.AutoMirrored icons. MDI carries no
per-icon RTL metadata, so mirroring is opted into per call site.toBitmap extensions for surfaces that cannot render Compose../gradlew :shared:updateMdiIcons regenerates the catalog from the version
pinned in gradle/libs.versions.toml, verified against the npm registry checksum. Renovate
watches the pin, and verifyMdiIcons fails CI until the catalog is regenerated after a bump.Shipping the catalog as Kotlin trades binary size against an updatable, multiplatform library. Measured on the Home Assistant Android app (release build), replacing Iconics and its typeface with this catalog:
| Metric | Impact | Notes |
|---|---|---|
| APK download size | +0.6 MB | The ~2.5 MB of path strings compress well; removing the 1.3 MB font offsets most of it |
| Raw dex (install size) | +4 MB | Each icon costs its path data plus roughly 800 bytes of dex structure for the accessor val
|
| RAM | pay per use | See below; the previous typeface enums held 1 to 1.5 MB of heap unconditionally |
The dex cost is the price of one accessor per icon. Icon fonts remain the most byte-efficient
encoding for mono-color icon sets (the font is memory mapped, so glyph shapes never touch the
Java heap either); this library deliberately spends those bytes on the canonical data format,
compile-time safety and ImageVector rendering.
The catalog is split into alphabetically sorted chunks, and a lookup loads only the chunk holding the requested name (binary search over the chunks' first names).
Measured retained heap on the JVM, first use from a cold catalog:
| Scenario | Retained |
|---|---|
| Single icon lookup | ~280 KiB |
| Six lookups spread across six different chunks (adversarial) | ~1.6 MiB |
Listing the full catalog through Mdi.icons (icon picker) |
~3.9 MiB |
Typical sessions resolve icons that cluster alphabetically, so real usage sits between the first two rows. The alias and rename tables load only when a lookup misses the canonical names.
ImageVectors are deliberately not cached in the library. rememberImageVector() scopes them to
the composition, so vectors exist only while their icon is on screen. A screen listing the whole
catalog pays for what it shows and releases it on dispose; nothing accumulates for the lifetime
of the process. toBitmap likewise returns a fresh bitmap on each call because its consumers
immediately parcel it to a system surface.
The library publishes to Maven Central as io.github.timoptr:mdi-icons through the
vanniktech maven publish plugin,
following the Kotlin Multiplatform publishing guide.
One publication covers every target: Android, iOS (arm64 and simulator), desktop (JVM), JS and wasm.
./gradlew :shared:updateMdiIcons # regenerate from the pinned @mdi/svg version
./gradlew :shared:verifyMdiIcons # check the generated catalog matches the pinThe version is pinned in gradle/libs.versions.toml under mdi-svg with a Renovate annotation.
The images in docs/images are Roborazzi screenshot tests of
the sample app, rendered with Robolectric:
./gradlew :sample:recordRoborazziAndroidHostTest # re-record after a sample UI change
./gradlew :sample:verifyRoborazziAndroidHostTest # check the images still match the app