
Jetpack Compose Material 3 1.5.0-alpha15 introduces a subtle but powerful refactor
MaterialTheme now uses a single LocalMaterialTheme CompositionLocal instead of separate locals for color, typography, shapes, and motion.
While this simplifies internal theme management, its real value emerges when building custom design systems and theme-aware Modifier libraries.
The Refactor: Cleaner Under the Hood
- Previously, MaterialTheme relied on multiple CompositionLocals:

- Now: Everything flows through one unified source:

This reduces allocations and simplifies the theme provider.
⭐️ Where It Shines: Design Systems & Custom Modifiers ⭐️
The game-changer is CompositionLocalConsumerModifierNode support.
Custom Modifiers that handle drawing/layout outside @Composable scopes can now read theme data directly:
object BrandGradientOverlayElement : ModifierNodeElement<BrandGradientOverlayNode>() {
override fun create(): BrandGradientOverlayNode = BrandGradientOverlayNode()
override fun update(node: BrandGradientOverlayNode) {
// No-Op
}
override fun InspectorInfo.inspectableProperties() {
name = "brandGradientOverlay"
}
override fun equals(other: Any?): Boolean = (this === other)
override fun hashCode(): Int = javaClass.hashCode()
}
class BrandGradientOverlayNode :
Modifier.Node(),
DrawModifierNode,
CompositionLocalConsumerModifierNode {
override fun ContentDrawScope.draw() {
// Read color scheme to access colors
val colorScheme =
currentValueOf(MaterialTheme.LocalMaterialTheme).colorScheme
val gradient = Brush.linearGradient(
listOf(
colorScheme.primary,
colorScheme.secondary)
)
drawContent()
drawRect(brush = gradient)
}
}
// Modifier
fun Modifier.brandGradientOverlay(): Modifier =
this then BrandGradientOverlayElement
Usage
@Composable
fun LocalMaterialThemeEx() {
MaterialTheme {
Card {
Box(
modifier = Modifier
.brandGradientOverlay()
) {
Text("Sample LocalMaterialTheme")
}
}
}
}

Before Unified LocalMaterialTheme
Before this change, CompositionLocalConsumerModifierNode couldn’t directly access the theme subsystems because there was no unified LocalMaterialTheme.
- Pass theme data via params
fun Modifier.brandGradientOverlay(
// Manual prop from parent composable
primaryColor: Color,
secondaryColor: Color
): Modifier = drawWithCache {
val brandGradient = Brush.linearGradient(
0f to primaryColor,
1f to secondaryColor
)
onDrawWithContent {
drawContent()
drawRect(brush = brandGradient)
}
}
// Usage
Card(
modifier = Modifier.brandGradientOverlay(
primary = MaterialTheme.colorScheme.primary,
secondary = MaterialTheme.colorScheme.secondary
)
)
- Capture from Composable (Recomposition-Heavy).
fun Modifier.brandGradientOverlay(): Modifier = composed {
// Captured here
val colorScheme = MaterialTheme.colorScheme
drawWithCache {
val brandGradient = Brush.linearGradient(
0f to colorScheme.primary,
1f to colorScheme.secondary
)
onDrawWithContent {
drawContent()
drawRect(brush = brandGradient)
}
}
}
❌ When Not to Use It
// ✅ DO: Regular composables
@Composable
fun MyButton() {
Button(colors = ButtonDefaults.colors(
containerColor = MaterialTheme.colorScheme.primary // Works unchanged
)) { }
}
// ❌ DON'T: Overcomplicate simple cases
@Composable
fun MyButton() {
// No need for LocalMaterialTheme.current here
val subsystems = MaterialTheme.LocalMaterialTheme.current
Button(colors = ButtonDefaults.colors(
containerColor = subsystems.colorScheme.primary
)) { }
}
References
Compose Material 3 | Jetpack | Android Developers
Keep in touch
https://www.linkedin.com/in/navczydev/
LocalMaterialTheme: From Prop Hell to Theme Nirvana — Material3 was originally published in ProAndroidDev on Medium, where people are continuing the conversation by highlighting and responding to this story.




This Post Has 0 Comments