Skip to content

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.kt containing MATERIAL_ICON_NAMES: List<String> (all filled icon names, sorted) and materialIconVector(name): ImageVector? resolving a name to its vector. Resolver whens are chunked (~370 branches each) to stay under the 64 KB JVM method limit. Regenerated from the library sources jar by android-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 — categoryIconVector falls back from the legacy when to materialIconVector to the Category icon. No schema change (still a TEXT column).
  • Picker: a ModalBottomSheet on the category editor hosts a search box and a LazyVerticalGrid over 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.
  • CategoryIcon call sites are unchanged (6 files) — only the resolver grew.
  • The catalog must be regenerated when material-icons-extended is bumped (documented in codebase-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.