skill: add Android skills

This commit is contained in:
2026-07-14 10:34:54 +08:00
parent e260c02629
commit ab82fb7cf0
182 changed files with 62992 additions and 0 deletions

View File

@@ -0,0 +1,459 @@
While Material is our recommended design system and Jetpack Compose ships an
implementation of Material, you are not forced to use it. Material is built
entirely on public APIs, so it's possible to create your own design system in
the same manner.
There are several approaches you might take:
- [Extend `MaterialTheme`](https://developer.android.com/develop/ui/compose/designsystems/custom#extending-material) with additional theming values.
- [Replace one or more Material systems](https://developer.android.com/develop/ui/compose/designsystems/custom#replacing-systems) --- `Colors`, `Typography`, or `Shapes` --- with custom implementations while keeping the others.
- [Implement a fully custom design system](https://developer.android.com/develop/ui/compose/designsystems/custom#implementing-fully-custom) to replace `MaterialTheme`.
You may also want to continue using Material components with a custom design
system. It's possible to do this but there are things to keep in mind to suit
the approach you've taken.
To learn more about the lower-level constructs and APIs used by `MaterialTheme`
and custom design systems, check out the [Anatomy of a theme in Compose](https://developer.android.com/develop/ui/compose/designsystems/anatomy) guide.
## Extend Material Theming
Compose Material closely models
[Material Theming](https://m3.material.io/)
to make it straightforward and type-safe to follow the Material guidelines.
However, it's possible to extend the color, typography, and shape sets with
additional values. The simplest approach is to add extension properties:
```kotlin
// Use with MaterialTheme.colorScheme.snackbarAction
val ColorScheme.snackbarAction: Color
@Composable
get() = if (isSystemInDarkTheme()) Red300 else Red700
// Use with MaterialTheme.typography.textFieldInput
val Typography.textFieldInput: TextStyle
get() = TextStyle(/* ... */)
// Use with MaterialTheme.shapes.card
val Shapes.card: Shape
get() = RoundedCornerShape(size = 20.dp)
```
<br />
This provides consistency with `MaterialTheme` usage APIs. An example of this
defined by Compose itself is
[`surfaceColorAtElevation`](https://developer.android.com/reference/kotlin/androidx/compose/material3/package-summary#(androidx.compose.material3.ColorScheme).surfaceColorAtElevation(androidx.compose.ui.unit.Dp)),
which determines the surface color that should be used depending on the
elevation.
> [!NOTE]
> **Note:** This approach is only recommended for straightforward theming value additions, or for values that are the same in different themes. If you have multiple themes, it's better to define a class with new properties instead.
Another approach is to define an extended theme that "wraps" `MaterialTheme` and
its values.
Suppose you want to add two additional colors --- `caution` and `onCaution`, a
yellow color used for actions that are semi-dangerous --- whilst keeping the
existing Material colors:
```kotlin
@Immutable
data class ExtendedColors(
val caution: Color,
val onCaution: Color
)
val LocalExtendedColors = staticCompositionLocalOf {
ExtendedColors(
caution = Color.Unspecified,
onCaution = Color.Unspecified
)
}
@Composable
fun ExtendedTheme(
/* ... */
content: @Composable () -> Unit
) {
val extendedColors = ExtendedColors(
caution = Color(0xFFFFCC02),
onCaution = Color(0xFF2C2D30)
)
CompositionLocalProvider(LocalExtendedColors provides extendedColors) {
MaterialTheme(
/* colors = ..., typography = ..., shapes = ... */
content = content
)
}
}
// Use with eg. ExtendedTheme.colors.caution
object ExtendedTheme {
val colors: ExtendedColors
@Composable
get() = LocalExtendedColors.current
}
```
<br />
This is similar to `MaterialTheme` usage APIs. It also supports multiple themes
as you can nest `ExtendedTheme`s in the same way as `MaterialTheme`.
### Use Material components
When extending Material Theming, existing `MaterialTheme` values are maintained
and Material components still have reasonable defaults.
If you want to use extended values in components, wrap them in your own
composable functions, directly setting the values you want to alter, and
exposing others as parameters to the containing composable:
```kotlin
@Composable
fun ExtendedButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable RowScope.() -> Unit
) {
Button(
colors = ButtonDefaults.buttonColors(
containerColor = ExtendedTheme.colors.caution,
contentColor = ExtendedTheme.colors.onCaution
/* Other colors use values from MaterialTheme */
),
onClick = onClick,
modifier = modifier,
content = content
)
}
```
<br />
You would then replace usages of `Button` with `ExtendedButton` where
appropriate.
```kotlin
@Composable
fun ExtendedApp() {
ExtendedTheme {
/*...*/
ExtendedButton(onClick = { /* ... */ }) {
/* ... */
}
}
}
```
<br />
## Replace Material subsystems
Instead of extending Material Theming, you may want to replace one or more
systems --- `Colors`, `Typography`, or `Shapes` --- with a custom implementation,
while maintaining the others.
Suppose you want to replace the type and shape systems while keeping the color
system:
```kotlin
@Immutable
data class ReplacementTypography(
val body: TextStyle,
val title: TextStyle
)
@Immutable
data class ReplacementShapes(
val component: Shape,
val surface: Shape
)
val LocalReplacementTypography = staticCompositionLocalOf {
ReplacementTypography(
body = TextStyle.Default,
title = TextStyle.Default
)
}
val LocalReplacementShapes = staticCompositionLocalOf {
ReplacementShapes(
component = RoundedCornerShape(ZeroCornerSize),
surface = RoundedCornerShape(ZeroCornerSize)
)
}
@Composable
fun ReplacementTheme(
/* ... */
content: @Composable () -> Unit
) {
val replacementTypography = ReplacementTypography(
body = TextStyle(fontSize = 16.sp),
title = TextStyle(fontSize = 32.sp)
)
val replacementShapes = ReplacementShapes(
component = RoundedCornerShape(percent = 50),
surface = RoundedCornerShape(size = 40.dp)
)
CompositionLocalProvider(
LocalReplacementTypography provides replacementTypography,
LocalReplacementShapes provides replacementShapes
) {
MaterialTheme(
/* colors = ... */
content = content
)
}
}
// Use with eg. ReplacementTheme.typography.body
object ReplacementTheme {
val typography: ReplacementTypography
@Composable
get() = LocalReplacementTypography.current
val shapes: ReplacementShapes
@Composable
get() = LocalReplacementShapes.current
}
```
<br />
### Use Material components
When one or more systems of `MaterialTheme` have been replaced, using Material
components as-is may result in unwanted Material color, type, or shape values.
If you want to use replacement values in components, wrap them in your own
composable functions, directly setting the values for the relevant system, and
exposing others as parameters to the containing composable.
> [!NOTE]
> **Note:** Not all values may be exposed as parameters in Material composables, in particular with `CompositionLocal` composables (such as `LocalTextStyle`). In such cases you may need to wrap `content` lambdas in provider functions (like `ProvideTextStyle`).
```kotlin
@Composable
fun ReplacementButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable RowScope.() -> Unit
) {
Button(
shape = ReplacementTheme.shapes.component,
onClick = onClick,
modifier = modifier,
content = {
ProvideTextStyle(
value = ReplacementTheme.typography.body
) {
content()
}
}
)
}
```
<br />
You would then replace usages of `Button` with `ReplacementButton` where
appropriate.
```kotlin
@Composable
fun ReplacementApp() {
ReplacementTheme {
/*...*/
ReplacementButton(onClick = { /* ... */ }) {
/* ... */
}
}
}
```
<br />
## Implement a fully custom design system
You may want to replace Material Theming with a fully custom design system.
Consider that `MaterialTheme` provides the following systems:
- `Colors`, `Typography`, and `Shapes`: Material Theming systems
- `TextSelectionColors`: Colors used for text selection by `Text` and `TextField`
- `Ripple` and `RippleTheme`: Material implementation of `Indication`
If you want to continue using Material components, you must replace some of
these systems in your custom themes or handle the systems in your
components to avoid unwanted behavior.
However, design systems are not limited to the concepts Material relies on. You
can modify existing systems and introduce entirely new ones --- with new classes
and types --- to make other concepts compatible with themes.
In the following code, we model a custom color system that includes gradients
(`List<Color>`), include a type system, introduce a new elevation system,
and exclude other systems provided by `MaterialTheme`:
![Screenshot of a mobile app UI demonstrating a custom design system with elements using gradients for colors, custom typography, and elevation.](https://developer.android.com/static/develop/ui/compose/images/themes/custom-color-gradients.png)
```kotlin
@Immutable
data class CustomColors(
val content: Color,
val component: Color,
val background: List<Color>
)
@Immutable
data class CustomTypography(
val body: TextStyle,
val title: TextStyle
)
@Immutable
data class CustomElevation(
val default: Dp,
val pressed: Dp
)
val LocalCustomColors = staticCompositionLocalOf {
CustomColors(
content = Color.Unspecified,
component = Color.Unspecified,
background = emptyList()
)
}
val LocalCustomTypography = staticCompositionLocalOf {
CustomTypography(
body = TextStyle.Default,
title = TextStyle.Default
)
}
val LocalCustomElevation = staticCompositionLocalOf {
CustomElevation(
default = Dp.Unspecified,
pressed = Dp.Unspecified
)
}
@Composable
fun CustomTheme(
/* ... */
content: @Composable () -> Unit
) {
val customColors = CustomColors(
content = Color(0xFFDD0D3C),
component = Color(0xFFC20029),
background = listOf(Color.White, Color(0xFFF8BBD0))
)
val customTypography = CustomTypography(
body = TextStyle(fontSize = 16.sp),
title = TextStyle(fontSize = 32.sp)
)
val customElevation = CustomElevation(
default = 4.dp,
pressed = 8.dp
)
CompositionLocalProvider(
LocalCustomColors provides customColors,
LocalCustomTypography provides customTypography,
LocalCustomElevation provides customElevation,
content = content
)
}
// Use with eg. CustomTheme.elevation.small
object CustomTheme {
val colors: CustomColors
@Composable
get() = LocalCustomColors.current
val typography: CustomTypography
@Composable
get() = LocalCustomTypography.current
val elevation: CustomElevation
@Composable
get() = LocalCustomElevation.current
}
```
<br />
### Use Material components
When no `MaterialTheme` is present, using Material components as-is will result
in unwanted Material color, type, and shape values and indication behavior.
If you want to use custom values in components, wrap them in your own composable
functions, directly setting the values for the relevant system, and exposing
others as parameters to the containing composable.
We recommend that you access values you set from your custom theme.
Alternatively, if your theme doesn't provide `Color`, `TextStyle`, `Shape`, or
other systems, you can hardcode them.
```kotlin
@Composable
fun CustomButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable RowScope.() -> Unit
) {
Button(
colors = ButtonDefaults.buttonColors(
containerColor = CustomTheme.colors.component,
contentColor = CustomTheme.colors.content,
disabledContainerColor = CustomTheme.colors.content
.copy(alpha = 0.12f)
.compositeOver(CustomTheme.colors.component),
disabledContentColor = CustomTheme.colors.content
.copy(alpha = 0.38f)
),
shape = ButtonShape,
elevation = ButtonDefaults.elevatedButtonElevation(
defaultElevation = CustomTheme.elevation.default,
pressedElevation = CustomTheme.elevation.pressed
/* disabledElevation = 0.dp */
),
onClick = onClick,
modifier = modifier,
content = {
ProvideTextStyle(
value = CustomTheme.typography.body
) {
content()
}
}
)
}
val ButtonShape = RoundedCornerShape(percent = 50)
```
<br />
> [!NOTE]
> **Note:** `Button` uses `rememberRipple()` internally to provide a `Ripple` `Indication`. It's a good idea to check the source code when implementing other custom components that wrap existing components.
If you've introduced new class types --- such as `List<Color>` to represent
gradients --- then it may be better to implement components from scratch instead
of wrapping them. For an example, take a look at
[`JetsnackButton`](https://github.com/android/compose-samples/blob/main/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Button.kt)
from the Jetsnack sample.
## Recommended for you
- Note: link text is displayed when JavaScript is off
- [Material Design 3 in Compose](https://developer.android.com/develop/ui/compose/designsystems/material3)
- [Migrate from Material 2 to Material 3 in Compose](https://developer.android.com/develop/ui/compose/designsystems/material2-material3)
- [Anatomy of a theme in Compose](https://developer.android.com/develop/ui/compose/designsystems/anatomy)

View File

@@ -0,0 +1,421 @@
There are three ways you can adopt Styles throughout your app:
1. Use directly on existing components that expose a [`Style`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/style/Style) parameter.
2. Apply a style with [`Modifier.styleable`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/style/styleable.modifier#(androidx.compose.ui.Modifier).styleable(androidx.compose.foundation.style.StyleState,androidx.compose.foundation.style.Style)) on layout composables that don't accept a `Style` parameter.
3. In your own custom design system, use `Modifier.styleable{}` and expose a style parameter on your own components.
## Available properties on Styles
Styles support many of the same properties that modifiers support; however, not
everything that is a modifier can be replicated with a Style. You still need
modifiers for certain behaviors, like interactions, custom drawing, or stacking
of properties.
| Grouping | Properties | Inherited by children |
|---|---|---|
| **Layout and sizing** | | |
| Content Padding (inner) | - `contentPadding(all: Dp)` - `contentPadding(horizontal: Dp, vertical: Dp)` - `contentPadding(start: Dp, top: Dp, end: Dp, bottom: Dp)` - `contentPaddingHorizontal(value: Dp)` / `contentPaddingVertical(value: Dp)` - `contentPaddingStart(value: Dp)` / `contentPaddingTop(value: Dp)` / `contentPaddingEnd(value: Dp)` / `contentPaddingBottom(value: Dp)` | No |
| External Padding (outer) | - `externalPadding(all: Dp)` - `externalPadding(horizontal: Dp, vertical: Dp)` - `externalPadding(start: Dp, top: Dp, end: Dp, bottom: Dp)` - `externalPaddingHorizontal(value: Dp)` / `externalPaddingVertical(value: Dp)` - `externalPaddingStart(value: Dp)` / `externalPaddingTop(value: Dp)` / `externalPaddingEnd(value: Dp)` / `externalPaddingBottom(value: Dp)` | No |
| Dimensions | `fillWidth()/fillHeight()/fillSize()` and `width`, `height`, and `size` (supports `Dp`, `DpSize`, or `Float` fractions). | No |
| Positioning | `left/top/right/bottom` offsets. | No |
| **Visual Appearance** | | |
| Fills | `background` and `foreground` (supports `Color` or `Brush`). | No |
| Borders | `borderWidth`, `borderColor`, and `borderBrush`. | No |
| Shape | `shape` | No - but used in conjunction with other properties. `clip` and `border` use this defined shape. |
| Shadows | `dropShadow`, `innerShadow` | No |
| **Transformations** | | |
| Graphics layer spatial movement | `translationX`, `translationY`, `scaleX/scaleY`, `rotationX/rotationY/rotationZ` | No |
| Control | `alpha`, `zIndex` (stacking order), and `transformOrigin` (pivot point) | No |
| **Typography** | | |
| Styling | `textStyle`, `fontSize`, `fontWeight`, `fontStyle`, and `fontFamily` | Yes |
| Coloration | `contentColor` and `contentBrush`. This is also used for Icons styling. | Yes |
| Paragraph | `lineHeight`, `letterSpacing`, `textAlign`, `textDirection`, `lineBreak`, and `hyphens`. | Yes |
| Decoration | `textDecoration`, `textIndent`, and `baselineShift`. | Yes |
## Use Styles directly on components with Style parameters
Components that expose a `Style` parameter allow you to set their styling:
```kotlin
BaseButton(
onClick = { },
style = { }
) {
BaseText("Click me")
}
```
<br />
Within the style lambda, you can set various properties, such as `externalPadding`
or `background`:
```kotlin
BaseButton(
onClick = { },
style = { background(Color.Blue) }
) {
BaseText("Click me")
}
```
<br />
For the full list of supported properties, see [Available properties on
Styles](https://developer.android.com/develop/ui/compose/styles/fundamentals#properties-styles).
## Apply Styles using modifiers for components with no existing parameter
For components that lack a built-in style parameter, you can still apply styles
with the `styleable` modifier. This approach is also useful when developing your
own custom components.
```kotlin
Row(
modifier = Modifier.styleable { }
) {
BaseText("Content")
}
```
<br />
Similar to the `style` parameter, you can include properties like `background`,
`contentPadding`, or `externalPadding` inside the lambda.
```kotlin
Row(
modifier = Modifier.styleable {
background(Color.Blue)
}
) {
BaseText("Content")
}
```
<br />
> [!NOTE]
> **Note:** When using `Modifier.styleable`, the child composables won't have those properties applied to them, unless they are inherited properties. Only the container with the `styleable` modifier has those properties applied.
Multiple chained `Modifier.styleable` modifiers are additive with non-inherited
properties on the applied composable, behaving similarly to multiple modifiers
defining the same properties. For inherited properties, these are overridden,
and the last `styleable` modifier in the chain sets the values.
When using `Modifier.styleable`, you may also want to create and supply a
`StyleState` to be used with the modifier to apply state-based styling. For more
details, see [State and animations with
Styles](https://developer.android.com/develop/ui/compose/styles/state-animations).
## Define a standalone Style
You can define a standalone Style for reusability purposes:
```kotlin
val style = Style { background(Color.Blue) }
```
<br />
You can then pass that defined style into a composable's style parameter or with
`Modifier.styleable`. When using `Modifier.styleable`, you also need to create a
`StyleState` object. `StyleState` is covered in detail in the [State and
animations with Styles](https://developer.android.com/develop/ui/compose/styles/state-animations) documentation.
The following example shows how you can apply a Style either directly through a
component's built-in parameters, or through a `Modifier.styleable`:
```kotlin
val style = Style { background(Color.Blue) }
// built in parameter
BaseButton(onClick = { }, style = style) {
BaseText("Button")
}
// modifier styleable
val styleState = remember { MutableStyleState(null) }
Column(
Modifier.styleable(styleState, style)
) {
BaseText("Column content")
}
```
<br />
You can also pass that Style into multiple components:
```kotlin
val style = Style { background(Color.Blue) }
// built in parameter
BaseButton(onClick = { }, style = style) {
BaseText("Button")
}
BaseText("Different text that uses the same style parameter", style = style)
// modifier styleable
val columnStyleState = remember { MutableStyleState(null) }
Column(
Modifier.styleable(columnStyleState, style)
) {
BaseText("Column")
}
val rowStyleState = remember { MutableStyleState(null) }
Row(
Modifier.styleable(rowStyleState, style)
) {
BaseText("Row")
}
```
<br />
## Add multiple Style properties
You can add multiple Style properties by setting different properties on each
line:
```kotlin
BaseButton(
onClick = { },
style = {
background(Color.Blue)
contentPaddingStart(16.dp)
}
) {
BaseText("Button")
}
```
<br />
> [!IMPORTANT]
> **Important:** Unlike modifier-based styling, properties in Styles override one another; the last property defined takes precedence.
Properties in Styles are not additive, unlike modifier-based styling. Styles
take the last set value in the list of properties within one style block. In the
following example, with the background set twice, the `TealColor` is the applied
background. For padding, `contentPaddingTop` overrides the top
padding set by `contentPadding` and does not combine the values.
```kotlin
BaseButton(
style = {
background(Color.Red)
// Background of Red is now overridden with TealColor instead
background(TealColor)
// All directions of padding are set to 64.dp (top, start, end, bottom)
contentPadding(64.dp)
// Top padding is now set to 16.dp, all other paddings remain at 64.dp
contentPaddingTop(16.dp)
},
onClick = {
//
}
) {
BaseText("Click me!")
}
```
<br />
![Button with two background colors set, and two contentPadding
overrides](https://developer.android.com/static/develop/ui/compose/styles/images/basic_style_button.png) **Figure 1.** Button with two background colors set and two `contentPadding` overrides.
## Merge multiple style objects
You can create multiple Style objects and pass them into the style parameter of
your composable.
```kotlin
val style1 = Style { background(TealColor) }
val style2 = Style { contentPaddingTop(16.dp) }
BaseButton(
style = style1 then style2,
onClick = {
},
) {
BaseText("Click me!")
}
```
<br />
![Button with background color and contentPaddingTop
set](https://developer.android.com/static/develop/ui/compose/styles/images/button_content_padding_top.png) **Figure 2.** Button with background color and `contentPaddingTop` set.
When multiple Styles specify the same property, the last set
property is chosen. Because properties are not additive in Styles, the last
padding passed in overrides the `contentPaddingHorizontal` set by the initial
`contentPadding`. Additionally, the last background color overrides the
background color set by the initial style passed in.
```kotlin
val style1 = Style {
background(Color.Red)
contentPadding(32.dp)
}
val style2 = Style {
contentPaddingHorizontal(8.dp)
background(Color.LightGray)
}
BaseButton(
style = style1 then style2,
onClick = {
},
) {
BaseText("Click me!")
}
```
<br />
In this case, the styling applied has a light gray background and `32.dp` padding,
except for the left and right padding, which has a value of `8.dp`.
![Button with contentPadding that's overridden by different
Styles](https://developer.android.com/static/develop/ui/compose/styles/images/button_content_padding_overrides.png) **Figure 3.** Button with `contentPadding` that's overridden by different Styles.
## Style inheritance
> [!NOTE]
> **Note:** While the Style APIs are experimental, you need to opt-in to enable Style inheritance by setting the flag `ComposeFoundationFlags.isInheritedTextStyleEnabled = true`.
Certain style properties, such as `contentColor` and text style-related
properties, propagate to the child composables. A style set on a child
composable overrides the inherited parent styling for that specific child.
![Style propagation with Style, styleable, and direct
parameters](https://developer.android.com/static/develop/ui/compose/styles/images/styles_modifiers_precedence_ordering.png) **Figure 4.** Style propagation with `Style`, `styleable`, and direct parameters.
| Priority | Method | Effect |
|---|---|---|
| 1 (Highest) | Direct arguments on a composable | Overrides everything; for example, `Text(color = Color.Red)` |
| 2 | Style parameter | Local style overrides `Text(style = Style { contentColor(Color.Red)}` |
| 3 | Modifier chain | `Modifier.styleable{ contentColor(Color.Red)` on the component itself. |
| 4 (Lowest) | Parent styles | For properties that can be inherited (Typography/Color) passed down from the parent. |
> [!NOTE]
> **Note:** Multiple chained `Modifier.styleable` modifiers are additive with non-inherited properties on the applied composable, similar to having multiple modifiers defining the same properties. For inherited properties, these are overridden; the last `styleable` modifier in the chain sets the values.
### Parent styling
You can set text properties (such as `contentColor`) from the parent composable,
and they propagate to all child `Text` composables.
```kotlin
val styleState = remember { MutableStyleState(null) }
Column(
modifier = Modifier.styleable(styleState) {
background(Color.LightGray)
val blue = Color(0xFF4285F4)
val purple = Color(0xFFA250EA)
val colors = listOf(blue, purple)
contentBrush(Brush.linearGradient(colors))
},
) {
BaseText("Children inherit", style = { width(60.dp) })
BaseText("certain properties")
BaseText("from their parents")
}
```
<br />
![Child composables' property
inheritance](https://developer.android.com/static/develop/ui/compose/styles/images/children_inherit_styles_parents.png) **Figure 5.** Child composables' property inheritance.
### Child override of properties
You can also set styling on a specific `Text` composable. If the parent composable
has styling set, the styling set on the child composable overrides the
parent composable's styling.
```kotlin
val styleState = remember { MutableStyleState(null) }
Column(
modifier = Modifier.styleable(styleState) {
background(Color.LightGray)
val blue = Color(0xFF4285F4)
val purple = Color(0xFFA250EA)
val colors = listOf(blue, purple)
contentBrush(Brush.linearGradient(colors))
},
) {
BaseText("Children can ", style = {
contentBrush(Brush.linearGradient(listOf(Color.Red, Color.Blue)))
})
BaseText("override properties")
BaseText("set by their parents")
}
```
<br />
![Child composables override parent
properties](https://developer.android.com/static/develop/ui/compose/styles/images/children_override_styles.png) **Figure 6.** Child composables override parent properties.
## Implement custom Style properties
You can create custom properties that map to existing Style definitions by using
extension functions on the `StyleScope`, as shown in the following example:
```kotlin
fun StyleScope.outlinedBackground(color: Color) {
border(1.dp, color)
background(color)
}
```
<br />
Apply this new property within a Style definition:
```kotlin
val customExtensionStyle = Style {
outlinedBackground(Color.Blue)
}
```
<br />
Creating new styleable properties is unsupported. If your use case
requires such support, submit a [feature request](https://issuetracker.google.com/issues/new?component=612128).
## Read `CompositionLocal` values
It's a common pattern to store design system tokens within a `CompositionLocal`,
to access the variables without needing to pass them as parameters. Styles
can access `CompositionLocal`s to retrieve system-wide values within a style:
```kotlin
val buttonStyle = Style {
contentPadding(12.dp)
shape(RoundedCornerShape(50))
background(Brush.verticalGradient(LocalCustomColors.currentValue.background))
}
```
<br />

View File

@@ -0,0 +1,461 @@
<br />
The Styles API offers a declarative and streamlined approach to managing UI
changes during interaction states like `hovered`, `focused`, and `pressed`. With
this API, you can significantly decrease the boilerplate code typically required
when using modifiers.
To facilitate reactive styling, `StyleState` acts as a stable, read-only
interface that tracks the active state of an element (such as its enabled,
pressed, or focused status). Within a `StyleScope`, you can access this through
the `state` property to implement conditional logic directly in your Style
definitions.
## State-based interaction: Hovered, focused, pressed, selected, enabled, toggled
Styles come with built-in support for common interactions:
- Pressed
- Hovered
- Selected
- Enabled
- Toggled
It's also possible to support custom states. See the [Custom State Styling with
StyleState](https://developer.android.com/develop/ui/compose/styles/state-animations#custom-state) section for more information.
### Handle interaction states with Style parameters
The following example demonstrates modifying the `background` and `borderColor`
in response to interaction states, specifically switching to purple when hovered
and blue when focused:
```kotlin
@Preview
@Composable
private fun OpenButton() {
BaseButton(
style = outlinedButtonStyle then {
background(Color.White)
hovered {
background(lightPurple)
border(2.dp, lightPurple)
}
focused {
background(lightBlue)
}
},
onClick = { },
content = {
BaseText("Open in Studio", style = {
contentColor(Color.Black)
fontSize(26.sp)
textAlign(TextAlign.Center)
})
}
)
}
```
<br />
**Figure 1.** Changing background color based on hovered and focused states.
You can also create nested state definitions. For example, you can define a
specific style for when a button is being both pressed and hovered
simultaneously:
```kotlin
@Composable
private fun OpenButton_CombinedStates() {
BaseButton(
style = outlinedButtonStyle then {
background(Color.White)
hovered {
// light purple
background(lightPurple)
pressed {
// When running on a device that can hover, whilst hovering and then pressing the button this would be invoked
background(lightOrange)
}
}
pressed {
// when running on a device without a mouse attached, this would be invoked as you wouldn't be in a hovered state only
background(lightRed)
}
focused {
background(lightBlue)
}
},
onClick = { },
content = {
BaseText("Open in Studio", style = {
contentColor(Color.Black)
fontSize(26.sp)
textAlign(TextAlign.Center)
})
}
)
}
```
<br />
**Figure 2.** Hovered and pressed state together on a button.
### Custom composables with Modifier.styleable
When creating your own `styleable` components, you must connect an
`interactionSource` to a `styleState`. Then, pass this state into
`Modifier.styleable` to utilize it.
Consider a scenario where your design system includes a `GradientButton`. You
may want to create a `LoginButton` that inherits from `GradientButton`, but
alters its colors during interactions, like being pressed.
- To enable `interactionSource` style updates, include an `interactionSource` as a parameter within your composable. Use the provided parameter or, if one is not supplied, initialize a new `MutableInteractionSource`.
- Initialize the `styleState` by providing the `interactionSource`. Make sure the `styleState`'s enabled status reflects the value of the provided enabled parameter.
- Assign the `interactionSource` to the `focusable` and `clickable` modifiers. Finally, apply the `styleState` to the modifier's `styleable` parameter.
```kotlin
@Composable
private fun GradientButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
style: Style = Style,
enabled: Boolean = true,
interactionSource: MutableInteractionSource? = null,
content: @Composable RowScope.() -> Unit,
) {
val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
val styleState = rememberUpdatedStyleState(interactionSource) {
it.isEnabled = enabled
}
Row(
modifier =
modifier
.clickable(
onClick = onClick,
enabled = enabled,
interactionSource = interactionSource,
indication = null,
)
.styleable(styleState, baseGradientButtonStyle then style),
content = content,
)
}
```
<br />
You can now use the `interactionSource` state to drive style modifications with
the pressed, focused, and hovered options inside the style block:
```kotlin
@Preview
@Composable
fun LoginButton() {
val loginButtonStyle = Style {
pressed {
background(
Brush.linearGradient(
listOf(Color.Magenta, Color.Red)
)
)
}
}
GradientButton(onClick = {
// Login logic
}, style = loginButtonStyle) {
BaseText("Login")
}
}
```
<br />
**Figure 3.** Changing a custom composable state based on `interactionSource`.
## Animate style changes
Styles state changes come with built-in animation support. You can wrap the new
property within any state change block with `animate` to automatically add
animations between different states. This is similar to the `animate*AsState`
APIs. The following example animates the `borderColor` from black to blue when
the state changes to focused:
```kotlin
val animatingStyle = Style {
externalPadding(48.dp)
border(3.dp, Color.Black)
background(Color.White)
size(100.dp)
pressed {
animate {
borderColor(Color.Magenta)
background(Color(0xFFB39DDB))
}
}
}
@Preview
@Composable
private fun AnimatingStyleChanges() {
val interactionSource = remember { MutableInteractionSource() }
val styleState = remember(interactionSource) { MutableStyleState(interactionSource) }
Box(modifier = Modifier
.clickable(
interactionSource,
enabled = true,
indication = null,
onClick = {
}
)
.styleable(styleState, animatingStyle)) {
}
}
```
<br />
**Figure 4.** Animating color changes on press.
The `animate` API accepts an `animationSpec` to change the duration or shape of
the animation curve. The following example animates the size of the box with a
`spring` spec:
```kotlin
val animatingStyleSpec = Style {
externalPadding(48.dp)
border(3.dp, Color.Black)
background(Color.White)
size(100.dp)
transformOrigin(TransformOrigin.Center)
pressed {
animate {
borderColor(Color.Magenta)
background(Color(0xFFB39DDB))
}
animate(spring(dampingRatio = Spring.DampingRatioMediumBouncy)) {
scale(1.2f)
}
}
}
@Preview(showBackground = true)
@Composable
fun AnimatingStyleChangesSpec() {
val interactionSource = remember { MutableInteractionSource() }
val styleState = remember(interactionSource) { MutableStyleState(interactionSource) }
Box(modifier = Modifier
.clickable(
interactionSource,
enabled = true,
indication = null,
onClick = {
}
)
.styleable(styleState, animatingStyleSpec))
}
```
<br />
**Figure 5.** Animating size and color changes on press.
## Custom state styling with StyleState
Depending on your composable use case, you may have different styles that are
backed by custom states. For example, if you have a media app, you may want to
have different styling for the buttons in your `MediaPlayer` composable
depending on the playback state of the player. Follow these steps to create and
use your own custom state:
1. Define custom key
2. Create `StyleState` extension
3. Link to custom state
### Define custom key
To create a custom state-based style, first create a
[`StyleStateKey`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/style/StyleStateKey) and pass in the default state value. When the
app launches, the media player is in the `Stopped` state, so it's initialized in
this way:
```kotlin
enum class PlayerState {
Stopped,
Playing,
Paused
}
val playerStateKey = StyleStateKey(PlayerState.Stopped)
```
<br />
### Create StyleState extension functions
Define an extension function on `StyleState` to query the current `playState`.
Then, create extension functions on `StyleScope` with your custom states passing
in the `playStateKey`, a lambda with the specific state, and the style.
```kotlin
// Extension Function on MutableStyleState to query and set the current playState
var MutableStyleState.playerState
get() = this[playerStateKey]
set(value) { this[playerStateKey] = value }
fun StyleScope.playerPlaying(block: () -> Unit) {
state(playerStateKey, block, { key, state -> state[key] == PlayerState.Playing })
}
fun StyleScope.playerPaused(block: () -> Unit) {
state(playerStateKey, block, { key, state -> state[key] == PlayerState.Paused })
}
```
<br />
### Link to custom state
Define the `styleState` in your composable and set the `styleState.playState`
equal to incoming state. Pass `styleState` into the `styleable` function on the
modifier.
```kotlin
@Composable
fun MediaPlayer(
url: String,
modifier: Modifier = Modifier,
style: Style = Style,
state: PlayerState = remember { PlayerState.Paused }
) {
// Hoist style state, set playstate as a parameter,
val styleState = remember { MutableStyleState(null) }
// Set equal to incoming state to link the two together
styleState.playerState = state
Box(
modifier = modifier.styleable(styleState, style)) {
///..
}
}
```
<br />
Within the `style` lambda, you can apply state-based styling for custom states,
using the previously defined extension functions.
```kotlin
@Composable
fun StyleStateKeySample() {
// Using the extension function to change the border color to green while playing
val style = Style {
borderColor(Color.Gray)
playerPlaying {
animate {
borderColor(Color.Green)
}
}
playerPaused {
animate {
borderColor(Color.Blue)
}
}
}
val styleState = remember { MutableStyleState(null) }
styleState[playerStateKey] = PlayerState.Playing
// Using the style in a composable that sets the state -> notice if you change the state parameter, the style changes. You can link this up to an ViewModel and change the state from there too.
MediaPlayer(url = "https://example.com/media/video",
style = style,
state = PlayerState.Stopped)
}
```
<br />
The following code is the full snippet for this example:
```kotlin
enum class PlayerState {
Stopped,
Playing,
Paused
}
val playerStateKey = StyleStateKey<PlayerState>(PlayerState.Stopped)
var MutableStyleState.playerState
get() = this[playerStateKey]
set(value) { this[playerStateKey] = value }
fun StyleScope.playerPlaying(block: () -> Unit) {
state(playerStateKey, block, { key, state -> state[key] == PlayerState.Playing })
}
fun StyleScope.playerPaused(block: () -> Unit) {
state(playerStateKey, block, { key, state -> state[key] == PlayerState.Paused })
}
@Composable
fun MediaPlayer(
url: String,
modifier: Modifier = Modifier,
style: Style = Style,
state: PlayerState = remember { PlayerState.Paused }
) {
// Hoist style state, set playstate as a parameter,
val styleState = remember { MutableStyleState(null) }
// Set equal to incoming state to link the two together
styleState.playerState = state
Box(
modifier = modifier.styleable(styleState, Style {
size(100.dp)
border(2.dp, Color.Red)
}, style, )) {
///..
}
}
@Composable
fun StyleStateKeySample() {
// Using the extension function to change the border color to green while playing
val style = Style {
borderColor(Color.Gray)
playerPlaying {
animate {
borderColor(Color.Green)
}
}
playerPaused {
animate {
borderColor(Color.Blue)
}
}
}
val styleState = remember { MutableStyleState(null) }
styleState[playerStateKey] = PlayerState.Playing
// Using the style in a composable that sets the state -> notice if you change the state parameter, the style changes. You can link this up to an ViewModel and change the state from there too.
MediaPlayer(url = "https://example.com/media/video",
style = style,
state = PlayerState.Stopped)
}
```
<br />

View File

@@ -0,0 +1,48 @@
Styles differ from modifiers by design. Styles don't replace modifiers; instead,
the two systems coexist with different goals. Internally, a Style is a modifier.
You can do everything Styles can do with modifiers, but not all functionality in
modifiers is available in Styles.
**Important:**
- **Choose Styles if:** You need to override a default of an existing component, perform high-performance animations, or define a theme-wide set of properties for a component.
- **Choose Modifiers if:** You need to add behavior (for example, clickable, gestures), define unique one-off layouts, or need additive properties.
The following is a comparison between Styles versus modifiers:
| Feature | Modifiers | Styles |
|---|---|---|
| **Primary Goal** | Define behaviors, semantics, and complex layouts. Modifiers manipulate individual elements on the fly for a particular composable and don't trickle down from the theme. | Define visual appearance, individual item sizing and themeable properties. Styles operate at a theme level and are over-writeable at a component level. They trickle down and apply styling across different composables. |
| **Logic** | Additive - the modifiers combine together to form a new result. | Over-writable - the last property set in the Style wins. Styles act as a single layer of properties that override each other based on a defined precedence hierarchy. |
| **Theming** | Challenging to lift into a theme, normally used individually. | By design, Styles are themeable (they can access `CompositionLocal`s) and can be defined once and used across components. |
| **Performance** | Updates often require all three phases of Compose: composition, layout and draw. Achieving good animation performance of modifiers often requires writing lambda-based versions. | Skips composition phase, only active in layout and draw phase, reducing recompositions. Requires less object allocation. |
| **Animations** | Requires using separate animation primitives like `animate*AsState` | Features built-in `animate { }` API that handles some animations for you. |
## Limitations of modifiers
Modifiers have many benefits in the current Compose landscape. However, Styles
address some limitations of modifiers, which the following list describes:
- Modifiers are typically created in the Composition phase. Updates can force a full rerun of Composition, Layout, and Draw, even for small visual changes like color, unless you create lambda-based modifiers.
- Conditional modifiers require disruptive if-else logic within fluent chains. Animating them requires manual state boilerplate and lacks a high-performance "auto-animate" mechanism.
- Modifiers stack rather than replace. You can't override a component's default border; you can only draw a second one on top.
- Modifiers are difficult to abstract into global themes. Consequently, themes usually store raw values instead of reusable modifier configurations.
## Limitations of Styles
While Styles can fill in some of the gaps that modifiers have, they also have
some limitations, which show how they cannot entirely replace modifiers:
- Styles are specialized Modifiers. While a modifier can do anything a Style does, the reverse is not true. Consequently, Styles can supplement, but cannot replace, modifiers.
- Styles are limited to visual configuration (backgrounds, padding, borders). They cannot handle behaviors like click logic, gesture detection, or accessibility semantics.
- Resolving a Style into its final state is *more expensive than applying a
single modifier*. The system must generate a data structure containing all possible property values, and the lookup of inherited properties further complicates this.
## When to use Styles over modifiers
While the choice to use Styles is largely dependent on your app and use cases,
the following guidance helps determine when to prefer a style over a modifier:
- **To achieve theme-wide consistency:** Styles are designed to be "lifted" into a global theme. Instead of passing repetitive Modifiers to every component, you can define a single Style in your theme to create a unified look across the entire app.
- **When performing frequent animations:** Styles evaluate during the Layout and Draw phases, allowing properties like color or scale to animate while bypassing the Composition phase entirely. This significantly reduces performance overhead. Use a Style instead of a modifier when doing visual property animations.
- **Overriding vs. stacking:** Use Styles when you need to replace a default property. Modifiers are additive (adding a border stacks a second one), whereas Styles use "last-write-wins" logic, making it easier to swap out backgrounds or padding without visual clutter.
- **Customizing Material components:** If a Material component provides a Style parameter, it is the suggested approach for customization. These styles allow you to access and modify specific properties within the composable's internal structure that might otherwise be inaccessible.

View File

@@ -0,0 +1,257 @@
> [!NOTE]
> **Note:** Styles are `@Experimental` and likely to change in upcoming releases, with Material support for Styles added in future releases. If you have any feedback, [file Styles issues](https://issuetracker.google.com/issues/new?component=612128).
There are several ways you can build out your apps using Styles. What you choose
depends on where your app sits in relation to its adoption of Material Design:
1. Fully custom design system, not using Material Design
- **Recommendation**: Define component styles that consume values from the theme, and expose style parameters on design system components.
2. Using Material Design
- **Recommendation**: Await Material adoption to integrate with Styles. Use styles on your own components where possible.
## The Style layer
In the traditional Compose model, customization often relies heavily on
overriding global tokens (colors and typography) provided by `MaterialTheme`, or
wrapping and overriding properties of a design system composable where possible.
Sometimes, there are properties within the Material layer that are not exposed
through the subsystems or parameters, but are hardcoded defaults on the
component itself.
With the Styles API, there's a new layer of abstraction that's a bridge between
subsystems and components: **Styles**.
| Layer | Responsibility | Example |
|---|---|---|
| **Subsystem values** | Named values | `val Primary = Color(0xFF34A85E)` |
| **Atomic Styles** | Style that does exactly one property change | `val largeSizeAtomic = Style { size(100.dp, 40.dp) }` |
| **Component Styles** | Component-specific configurations | A Button with Primary background and 16dp padding. `val buttonStyle = Style { contentPadding(16.dp) shape(RoundedCornerShape(8.dp)) background(Color.Blue) }` |
| **Components** | The functional UI element that consumes a Style. | `Button(style = buttonStyle) { ... }` |
![Diagram showing Theming with Styles with the new layer introduction](https://developer.android.com/static/develop/ui/compose/styles/images/theming_styles_layer.png) **Figure 1.** An example of a component and how it accesses styles from a theme.
### Atomic versus monolithic Styles
With the Styles API, you can break down a Style into separate atomic styles.
Instead of defining complex, component-specific styles like `baseButtonStyle`,
you can also create small, single-purpose utility styles. These act as your
"atoms".
```kotlin
// Define single-purpose "atomic" styles
val paddingAtomic = Style {
contentPadding(16.dp)
}
val roundedCornerShapeAtomic = Style {
shape(RoundedCornerShape(8.dp))
}
val primaryBackgroundAtomic = Style {
background(Color.Blue)
}
val largeSizeAtomic = Style {
size(100.dp, 40.dp)
}
val interactiveShadowAtomic = Style {
hovered {
animate {
dropShadow(
Shadow(
offset = DpOffset(
0.dp,
0.dp
),
radius = 2.dp,
spread = 0.dp,
color = Color.Blue,
)
)
}
}
}
```
<br />
#### Composition using "then"
One of the powerful features of the new Styles API is the `then` operator, which
lets you merge multiple `Style` objects. This lets you build a component using
atomic utility classes.
**Traditional (non-atomic)**:
```kotlin
// One large monolithic style
val buttonStyle = Style {
contentPadding(16.dp)
shape(RoundedCornerShape(8.dp))
background(Color.Blue)
}
```
<br />
**Atomic refactor**:
```kotlin
// Combine atoms to create the final appearance
val buttonStyle = paddingAtomic then roundedCornerShapeAtomic then primaryBackgroundAtomic then interactiveShadowAtomic
```
<br />
## Adopt Styles in your design system
Consider the following options when adopting Styles within your design system,
depending on where in the spectrum your design system lies.
### Custom design system with Styles
***Consider when**: You've been handed an extensive brand guide that is not
based on Material Design, and you are not planning to use Material Design*.
***Strategy**: Implement a fully custom design system, and expose styles as part
of the theme*.
This option is the custom path if you don't use Material as your main design
system language. You bypass `MaterialTheme` entirely for visual definitions and
have created your [own custom theme already](https://developer.android.com/develop/ui/compose/designsystems/custom#implementing-fully-custom). You build a `CompanyTheme` that
acts as a container for your Styles.
- **How it works** : Create a `CompanyTheme` object that holds `Style` objects for every component in your system. Your components (either wrappers around Material logic or custom `Box` or `Layout` implementations) consume these styles directly, and expose a `Style` parameter for consumers of your design system.
- **The Style layer**: Styles are the primary definition of your design system. Tokens are named variables fed into these styles. This allows for deep customization, such as defining unique animations for state changes (for example, animating scale and color on press).
If you are building out your own [custom theme](https://developer.android.com/develop/ui/compose/designsystems/custom) without using Material, and
want to adopt styles, add your list of styles to your Theme. This lets you
access your base styles from anywhere in your project.
1. Create a `Styles` class that stores the various styles in your application
and create the defaults. For example, in the Jetsnack app - the class is
named `JetsnackStyles`:
```kotlin
object JetsnackStyles{
val buttonStyle: Style = Style {
shape(shapes.medium)
background(colors.brand)
contentColor(colors.textPrimary)
contentPaddingVertical(8.dp)
contentPaddingHorizontal(24.dp)
textStyle(typography.labelLarge)
disabled {
animate {
background(colors.brandSecondary)
}
}
}
val cardStyle: Style = Style {
shape(shapes.medium)
background(colors.uiBackground)
contentColor(colors.textPrimary)
}
}
```
<br />
2. Provide `Styles` as part of your overall theme, and expose helper extension
functions on `StyleScope` to access the subsystems:
```kotlin
@Immutable
class JetsnackTheme(
val colors: JetsnackColors = LightJetsnackColors,
val typography: androidx.compose.material3.Typography = androidx.compose.material3.Typography(),
val shapes: Shapes = Shapes()
) {
companion object {
val colors: JetsnackColors
@Composable @ReadOnlyComposable
get() = LocalJetsnackTheme.current.colors
val typography: androidx.compose.material3.Typography
@Composable @ReadOnlyComposable
get() = LocalJetsnackTheme.current.typography
val shapes: Shapes
@Composable @ReadOnlyComposable
get() = LocalJetsnackTheme.current.shapes
val styles: JetsnackStyles = JetsnackStyles
val LocalJetsnackTheme: ProvidableCompositionLocal<JetsnackTheme>
get() = LocalJetsnackThemeInstance
}
}
val StyleScope.colors: JetsnackColors
get() = LocalJetsnackTheme.currentValue.colors
val StyleScope.typography: androidx.compose.material3.Typography
get() = LocalJetsnackTheme.currentValue.typography
val StyleScope.shapes: Shapes
get() = LocalJetsnackTheme.currentValue.shapes
internal val LocalJetsnackThemeInstance = staticCompositionLocalOf { JetsnackTheme() }
@Composable
fun JetsnackTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
val colors = if (darkTheme) DarkJetsnackColors else LightJetsnackColors
val theme = JetsnackTheme(colors = colors)
CompositionLocalProvider(
LocalJetsnackTheme provides theme,
) {
MaterialTheme(
typography = LocalJetsnackTheme.current.typography,
shapes = LocalJetsnackTheme.current.shapes,
content = content,
)
}
}
```
<br />
3. Access `JetsnackStyles` within your composable:
```kotlin
@Composable
fun CustomButton(modifier: Modifier,
style: Style = Style,
text: String) {
val interactionSource = remember { MutableInteractionSource() }
val styleState = remember(interactionSource) { MutableStyleState(interactionSource) }
// Apply style to top level container in combination with incoming style from parameter.
Box(modifier = modifier
.clickable(
interactionSource = interactionSource,
indication = null,
enabled = true,
role = Role.Button,
onClick = {
},
)
.styleable(styleState, JetsnackTheme.styles.buttonStyle, style)) {
Text(text)
}
}
```
<br />
Beyond global theme adoption, there are alternative strategies for incorporating
`Styles` into your apps. You can leverage `Styles` inline for specific call
sites or use static definitions when full theming capabilities are unnecessary.
`Styles` shouldn't be swapped conditionally unless the whole style is
fundamentally different. You should prefer accessing dynamic tokens inside a
visual definition rather than switching between distinct style objects.