Refining Compose API for Design Systems
Article Summary
Yury from Bumble Tech tackles a problem every Android team faces: how do you build design system components that are both easy to use and flexible enough for edge cases?
Building custom Compose components for design systems requires balancing strict guidelines with real-world flexibility. This article walks through evolving a NavigationBar component from restrictive to extensible, using techniques borrowed from Material Design 3's own implementation patterns.
Key Takeaways
- Restrictive APIs enforce consistency but break when new requirements emerge
- Slot APIs with Defaults objects solve discoverability and scope pollution
- Scoping lambdas eliminate parameter duplication across component configurations
- Separate scopes per slot prevent misuse while maintaining extensibility
The relaxed with Defaults approach delivers extensible components that enforce design system rules without requiring constant API updates for new use cases.
About This Article
Design system components in Jetpack Compose involve a difficult choice. Restrictive APIs stop developers from making mistakes, but they need constant updates whenever new use cases come up. Relaxed APIs let developers do more, but they can pollute the global scope and create styling inconsistencies.
Yury's team used a Defaults object pattern with scoped lambdas. They converted NavigationBarDefaults into a receiver type for composable lambdas. This lets developers access styling functions through the `this` scope instead of typing out explicit prefixes.
The scoping approach cut down on repeated parameters. ButtonSize only needs to be set once in the Button component instead of being repeated for each Text call. Separate NavigationBarContentScope and NavigationBarButtonScope classes also prevent developers from accidentally using IconButton in the wrong content slots.