Temas com estilos

Há várias maneiras de criar apps usando estilos. A escolha depende da posição do app em relação à adoção do Material Design:

  1. Sistema de design totalmente personalizado, sem usar o Material Design
    • Recomendação: defina estilos de componentes que consomem valores do tema e exponha parâmetros de estilo em componentes do sistema de design.
  2. Usando o Material Design
    • Recomendação: aguarde a adoção do Material para integrar com estilos. Use estilos nos seus próprios componentes sempre que possível.

A camada de estilo

No modelo tradicional do Compose, a personalização geralmente depende muito da substituição de tokens globais (cores e tipografia) fornecidos pelo MaterialTheme ou do encapsulamento e substituição de propriedades de um elemento combinável do sistema de design sempre que possível. Às vezes, há propriedades na camada do Material que não são expostas pelos subsistemas ou parâmetros, mas são padrões codificados no próprio componente.

Com a API Styles, há uma nova camada de abstração que é uma ponte entre subsistemas e componentes: estilos.

Camada Responsabilidade Exemplo
Valores do subsistema Valores nomeados val Primary = Color(0xFF34A85E)
Estilos atômicos Estilo que faz exatamente uma mudança de propriedade val buttonStyle = paddingAtomic then roundedCornerShapeAtomic then primaryBackgroundAtomic then largeSize then interactiveShadowAtomic
Estilos de componentes Configurações específicas do componente Um botão com plano de fundo principal e preenchimento de 16 dp. val buttonStyle = Style { contentPadding(16.dp) shape(RoundedCornerShape(8.dp)) background(Color.Blue) }
Componentes O elemento da interface funcional que consome um estilo. Button(style = buttonStyle) { ... }
Diagrama mostrando temas com estilos e a nova introdução de camadas
Figura 1. Um exemplo de um componente e como ele acessa estilos de um tema.

Estilos atômicos x monolíticos

Com a API Styles, é possível dividir um estilo em estilos atômicos separados. Em vez de definir estilos complexos e específicos de componentes, como baseButtonStyle, também é possível criar estilos de utilitários pequenos e de uso único. Eles atuam como seus "átomos".

// 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,
                )
            )
        }
    }
}

Composição usando "then"

Um dos recursos avançados da nova API Styles é o operador then, que permite mesclar vários objetos Style. Isso permite criar um componente usando classes de utilitários atômicos.

Tradicional (não atômico):

// One large monolithic style
val buttonStyle = Style {
    contentPadding(16.dp)
    shape(RoundedCornerShape(8.dp))
    background(Color.Blue)
}

Refatoração atômica:

// Combine atoms to create the final appearance
val buttonStyle = paddingAtomic then roundedCornerShapeAtomic then primaryBackgroundAtomic then interactiveShadowAtomic

Adotar estilos no sistema de design

Considere as opções a seguir ao adotar estilos no sistema de design, dependendo de onde ele está no espectro.

Sistema de design personalizado com estilos

Considerar quando: você recebeu um guia de marca abrangente que não é baseado no Material Design e não planeja usar o Material Design.

Estratégia: implemente um sistema de design totalmente personalizado e exponha estilos como parte do tema.

Essa opção é o caminho personalizado se você não usar o Material como a linguagem principal do sistema de design. Você ignora o MaterialTheme completamente para definições visuais e já criou seu próprio tema personalizado. Você cria um CompanyTheme que atua como um contêiner para seus estilos.

  • Como funciona: crie um objeto CompanyTheme que contenha objetos Style para cada componente do sistema. Seus componentes (wrappers em torno da lógica do Material ou implementações personalizadas de Box ou Layout) consomem esses estilos diretamente e expõem um parâmetro Style para consumidores do sistema de design.
  • A camada de estilo: os estilos são a definição principal do sistema de design. Os tokens são variáveis nomeadas alimentadas nesses estilos. Isso permite uma personalização profunda, como definir animações exclusivas para mudanças de estado (por exemplo, animação de escala e cor na pressão).

Se você estiver criando seu próprio tema personalizado sem usar o Material e quiser adotar estilos, adicione sua lista de estilos ao tema. Isso permite acessar seus estilos de base de qualquer lugar no projeto.

  1. Crie uma classe Styles que armazene os vários estilos no aplicativo e crie os padrões. Por exemplo, no app Jetsnack, a classe é chamada de JetsnackStyles:

    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)
        }
    }

  2. Forneça Styles como parte do tema geral e exponha funções de extensão auxiliares em StyleScope para acessar os subsistemas:

    @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,
            )
        }
    }

  3. Acesse JetsnackStyles no elemento combinável:

    @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)
        }
    }

Além da adoção global de temas, há estratégias alternativas para incorporar Styles aos apps. É possível aproveitar Styles inline para sites de chamadas específicos ou usar definições estáticas quando os recursos de tema completos não forem necessários. Styles não deve ser trocado condicionalmente, a menos que o estilo inteiro seja fundamentalmente diferente. É preferível acessar tokens dinâmicos dentro de uma definição visual em vez de alternar entre objetos de estilo distintos.