ADR-0023: Full Material icon catalog for category icons
- Status: Accepted
- Date: 2026-08-15
- Deciders: maintainers
- Related: ADR-0022
Context
The category editor offered only ten hand-picked icons (AVAILABLE_CATEGORY_ICONS),
shown as chips. Users wanted the full icon set with a search box, without
materializing every vector up front.
material-icons-extended (1.7.8) exposes each of its 2083 "filled" icons as a
lazy extension property (Icons.Filled.Fastfood, name = "Filled.Fastfood").
There is no built-in enumerable list (AllIcons does not exist), and each
vector is built only when its getter is accessed and then cached. Icons were
stored as custom short ids ("fastfood") resolved by categoryIconVector.
Decision
- Generate a catalog: a checked-in
MaterialIconCatalog.ktcontainingMATERIAL_ICON_NAMES: List<String>(all filled icon names, sorted) andmaterialIconVector(name): ImageVector?resolving a name to its vector. Resolverwhens are chunked (~370 branches each) to stay under the 64 KB JVM method limit. Regenerated from the library sources jar byandroid-app/scripts/generate_icon_catalog.sh. - Storage: new custom categories store the canonical
ImageVector.name("Filled.Fastfood"). Legacy seeds and existing rows keep their short ids —categoryIconVectorfalls back from the legacywhentomaterialIconVectorto theCategoryicon. No schema change (still aTEXTcolumn). - Picker: a
ModalBottomSheeton the category editor hosts a search box and aLazyVerticalGridover the filtered catalog. Rendering is lazy — only visible tiles inflate their vector, so broad searches stay cheap. The selected tile is highlighted by vector identity (materialIconVector(name) === categoryIconVector(current)), which also highlights legacy short-id icons. - Search is a pure
filterIconNames(query, names): case-insensitive substring over both the human label ("shopping cart" ←ShoppingCart) and the raw name.
Consequences
- New categories can use any of the 2083 filled Material icons; defaults keep their seeded ids.
CategoryIconcall sites are unchanged (6 files) — only the resolver grew.- The catalog must be regenerated when
material-icons-extendedis bumped (documented incodebase-map.md). - AutoMirrored duplicates are intentionally excluded; the deprecated-Filled
variants are
@file:Suppressd in the generated file. - The generated file is large (~6300 lines) but compile-friendly (chunked).
Alternatives considered
- Reflection / runtime enumeration of icons — not feasible: no enumerable API exists and R8 strips metadata.
- Curated larger subset — contradicts "all icons available".
- Generated resolver via map of lambdas — rejected in favor of a chunked
when: no eager map allocation and R8-friendly.