skill: add Android skills
This commit is contained in:
@@ -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`:
|
||||
|
||||

|
||||
|
||||
|
||||
```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)
|
||||
@@ -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 />
|
||||
|
||||
 **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 />
|
||||
|
||||
 **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`.
|
||||
 **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.
|
||||
 **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 />
|
||||
|
||||
 **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 />
|
||||
|
||||
 **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 />
|
||||
@@ -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 />
|
||||
@@ -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.
|
||||
@@ -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) { ... }` |
|
||||
|
||||
 **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.
|
||||
Reference in New Issue
Block a user