Temas do Jetpack Compose

1. Introdução

Neste codelab, você vai aprender a usar as APIs de temas do Jetpack Compose para estilizar seu aplicativo. Vamos ver como personalizar cores, formas e tipografia para que sejam usadas de forma consistente em todo o app, oferecendo suporte a vários temas, como temas claros e escuros.

O que você vai aprender

Neste codelab, você vai aprender o seguinte:

  • Uma introdução ao Material Design e como ele pode ser usado e personalizado de acordo com sua marca.
  • Como o Compose implementa o sistema do Material Design.
  • Como definir e usar cores, tipografia e formas em todo o app.
  • Como definir o estilo dos componentes.
  • Como oferecer suporte a temas claros e escuros.

O que você vai criar

Neste codelab, vamos estilizar um app de leitura de notícias. Começamos com um aplicativo sem estilo e usaremos o que aprendemos para aplicar um tema a ele e oferecer suporte a temas escuros.

Imagem mostrando o Jetnews, um app de leitura de notícias, antes de aplicar estilos.

Imagem mostrando o Jetnews, um app de leitura de notícias, depois de aplicar estilos.

Imagem mostrando o Jetnews, um app de leitura de notícias, com um tema escuro.

Antes: app sem estilo

Depois: app com estilo

Depois: tema escuro

Prerequisites

2. Etapas da configuração

Nesta etapa, você vai fazer o download do código, que inclui um app simples para leitura de notícias que vamos estilizar.

O que é necessário

Fazer o download do código

Se você tiver o git instalado, basta executar o comando abaixo. Para verificar se o git está instalado, digite git --version no terminal ou na linha de comando e verifique se ele é executado corretamente.

git clone https://github.com/googlecodelabs/android-compose-codelabs.git
cd android-compose-codelabs/ThemingCodelab

Caso você não tenha o git, clique no botão abaixo para fazer o download de todo o código para este codelab:

Abra o projeto no Android Studio Arctic Fox ou em uma versão mais recente, selecione "File > Import Project" e navegue até o diretório ThemingCodelab.

O projeto contém três pacotes principais:

  • com.codelab.theming.data contém classes de modelo e dados de exemplo. Não é necessário editar esse pacote durante o codelab.
  • com.codelab.theming.ui.start é o ponto de partida do codelab. Você precisa fazer todas as mudanças explicadas no codelab neste pacote.
  • com.codelab.theming.ui.finish é o estado final do codelab, para referência.

Criar e executar o app

O aplicativo tem duas configurações de execução que refletem os estados inicial e final do codelab. Selecionar qualquer uma delas e pressionar o botão de execução vai implantar o código no dispositivo ou emulador.

e96bac2ac3298e91.png

O aplicativo também contém Visualizações de layout do Compose. Navegar para Home.kt, seja no pacote start ou no finish, e abrir a visualização de design mostra várias visualizações que permitem iterações rápidas no código da IU:

758a285ad8a6cd51.png

3. Temas do Material Design

O Jetpack Compose oferece uma implementação do Material Design, um sistema de design abrangente para a criação de interfaces digitais. Os componentes do Material Design (botões, cards, chaves etc.) são baseados nos Temas do Material Design, uma maneira sistemática de personalizar o Material Design para refletir melhor a marca do seu produto. Um tema do Material é composto por atributos de cor, tipografia e forma. Essa personalização é refletida automaticamente nos componentes usados para criar o app.

Conhecer os temas do Material Design é útil para entender como definir temas em apps do Jetpack Compose. Veja uma breve descrição dos conceitos. Se você já conhece os Temas do Material Design, pode pular para frente.

Cor

O Material Design define várias cores (link em inglês) com nomes semânticos e podem ser usadas em todo o app:

bb8ab0b2d8f9bca8.png

A cor "primary" (primária) é a cor principal da sua marca, e a "secondary" (secundária) é usada para destaques. Você pode fornecer variantes mais escuras ou mais claras para áreas contrastantes. As cores de "background" (plano de fundo) e "surface" (superfície) são usadas para contêineres de componentes que, internamente, ficam em uma "superfície" do aplicativo. O Material Design também define cores como "on", que são usadas no conteúdo em cima de uma das cores nomeadas. Por exemplo, um texto em um contêiner colorido como "surface", precisa ser colorido como "on surface" (na superfície). Os componentes do Material Design estão configurados para usar essas cores do tema. Por exemplo, a cor de um botão de ação flutuante é definida como secondary, a de Cards é definida como surface por padrão e assim por diante (links em inglês).

Ao definir cores com nomes, é possível fornecer paletas de cores alternativas, como um tema claro e um escuro:

8df1a7854a5ca1f2.png

Isso também incentiva que você defina uma pequena paleta de cores e que as use de forma consistente em todo o app. A ferramenta de cores do Material Design (link em inglês) ajuda você a escolher cores e criar uma paleta, além de garantir que as combinações sejam acessíveis.

Tipografia

Da mesma forma, o Material Design define (link em inglês) vários estilos para tipos com nomes semânticos:

767fd40cb6938dc4.png

Embora não seja possível variar os estilos de tipografia por tema, o uso de uma escala promove a consistência dentro do aplicativo. O fornecimento das suas próprias fontes e outras personalizações de tipo será refletido nos componentes do Material Design usados no app, por exemplo, as barras de apps usam o estilo h6 por padrão, o uso de botões, por exemplo, button. A ferramenta geradora de escala de tipo do Material Design pode ajudar você a criar sua escala de tipo.

Forma

O Material Design oferece suporte sistemático ao uso de formas (link em inglês) que representam sua marca. Ele define três categorias: componentes pequenos, médios e grandes. Cada uma delas pode definir uma forma a ser usada, personalizando o estilo do canto (cortado ou arredondado) e o tamanho.

8a795ddff8f8cf5f.png

A personalização do tema da forma será refletida em vários componentes, como botões e campos de texto que usam o tema pequeno, os cards e as caixas de diálogo usam mídia, e o Planilhas usa o tema grande, por padrão. Veja mapeamento completo de componentes para componentes. A ferramenta de personalização de formas do Material Design pode ajudar você a gerar um tema de forma.

Referência

O Material Design tem como padrão um tema de referência, que usa o esquema de cores roxo, a escala tipográfica Roboto e formas ligeiramente arredondadas, como visto nas imagens acima. Se você não especificar ou personalizar o tema, os componentes vão usar o tema de referência.

4. Definir o tema

MaterialTheme

O principal elemento para implementar temas no Jetpack Compose é o elemento MaterialTheme que pode ser composto. Colocar esse elemento na hierarquia do Compose permite que você especifique personalizações de cor, tipografia e de formas para todos os componentes. Veja como esse elemento é definido na biblioteca:

@Composable
fun MaterialTheme(
    colors: Colors,
    typography: Typography,
    shapes: Shapes,
    content: @Composable () -> Unit
) { ...

Depois, é possível acessar os parâmetros transmitidos a esse elemento que pode ser composto usando o MaterialTheme object, que expõe as propriedades colors, typography e shapes. Vamos ver detalhes sobre cada um deles mais tarde.

Abra Home.kt e localize a função de composição Home. Esse é o ponto de entrada principal do app. Enquanto declaramos um MaterialTheme, não especificamos nenhum parâmetro, portanto, recebemos o estilo de referência padrão:

@Composable
fun Home() {
  ...
  MaterialTheme {
    Scaffold(...

Vamos criar parâmetros de cor, tipografia e forma para implementar um tema no app.

Criar um tema

Para centralizar o estilo, recomendamos que você crie seu próprio elemento que pode ser composto para encapsular e configurar um MaterialTheme. Dessa forma, há apenas um local para especificar as personalizações do tema e você pode reutilizar esse tema facilmente em muitos lugares, como em várias telas ou @Previews. Se necessário, é possível criar vários elementos que podem ser compostos para o tema. Por exemplo, caso você queira oferecer suporte a diferentes estilos em diferentes seções do app.

No pacote com.codelab.theming.ui.start.theme, crie um novo arquivo Kotlin com o nome Theme.kt. Adicione uma nova função de composição com o nome JetnewsTheme, que aceita como conteúdo outros elementos que podem ser compostos e encapsula um MaterialTheme:

@Composable
fun JetnewsTheme(content: @Composable () -> Unit) {
  MaterialTheme(content = content)
}

Agora, volte para Home.kt, substitua o MaterialTheme pelo JetnewsTheme e o importe:

-  MaterialTheme {
+  JetnewsTheme {
    ...

Ainda não é possível ver as mudanças na @Preview nesta tela. Atualize a PostItemPreview e a FeaturedPostPreview para unir o conteúdo com o novo elemento JetnewsTheme que pode ser composto, para que as visualizações usem o novo tema:

@Preview("Featured Post")
@Composable
private fun FeaturedPostPreview() {
  val post = remember { PostRepo.getFeaturedPost() }
+ JetnewsTheme {
    FeaturedPost(post = post)
+ }
}

Cores

Esta é a paleta de cores que queremos implementar no app. Por enquanto, há uma paleta apenas para o tema claro, vamos oferecer suporte ao tema escuro em breve:

16a0a3d57f49b71d.png

As cores no Compose são definidas usando a classe Color. Há vários construtores que permitem especificar a cor como um ULong ou por um canal de cores separado.

Crie um novo arquivo Color.kt no pacote theme. Adicione as cores abaixo como propriedades públicas de alto nível neste arquivo:

val Red700 = Color(0xffdd0d3c)
val Red800 = Color(0xffd00036)
val Red900 = Color(0xffc20029)

Agora que definimos as cores do nosso app, vamos reuni-las em um objeto Colors exigido pelo MaterialTheme, atribuindo cores específicas às cores nomeadas do Material Design. Volte para Theme.kt e adicione o código abaixo:

private val LightColors = lightColors(
    primary = Red700,
    primaryVariant = Red900,
    onPrimary = Color.White,
    secondary = Red700,
    secondaryVariant = Red900,
    onSecondary = Color.White,
    error = Red800
)

Aqui, usamos a função lightColors para criar a classe Colors. Ela fornece padrões sensíveis para que não precisemos especificar todas as cores que compõem uma paleta do Material Design. Por exemplo, observe que não especificamos uma cor background ou muitas das cores "on" já que vamos usar as cores padrão.

Agora, vamos usar essas cores no nosso app. Atualize o elemento que pode ser composto JetnewsTheme para usar nossas novas Colors:

@Composable
fun JetnewsTheme(content: @Composable () -> Unit) {
  MaterialTheme(
+   colors = LightColors,
    content = content
  )
}

Abra Home.kt e atualize a visualização. Observe o novo esquema de cores refletido em componentes como a TopAppBar.

Tipografia

Veja a escala tipográfica que queremos implementar no nosso app:

985064b5f0dbd8bd.png

No Compose, podemos definir objetos TextStyle a fim de estabelecer as informações necessárias para estilizar algum texto. Um exemplo dos atributos dele:

data class TextStyle(
    val color: Color = Color.Unset,
    val fontSize: TextUnit = TextUnit.Inherit,
    val fontWeight: FontWeight? = null,
    val fontStyle: FontStyle? = null,
    val fontFamily: FontFamily? = null,
    val letterSpacing: TextUnit = TextUnit.Inherit,
    val background: Color = Color.Unset,
    val textAlign: TextAlign? = null,
    val textDirection: TextDirection? = null,
    val lineHeight: TextUnit = TextUnit.Inherit,
    ...
)

A escala tipográfica que queremos usa Montserrat para títulos e Domine para o corpo do texto. Os arquivos de fontes relevantes já foram adicionados à pasta res/fonts do projeto.

Crie um novo arquivo com o nome Typography.kt no pacote theme: Primeiro, vamos definir as FontFamilys, que combinam os diferentes pesos de cada Font:

private val Montserrat = FontFamily(
    Font(R.font.montserrat_regular),
    Font(R.font.montserrat_medium, FontWeight.W500),
    Font(R.font.montserrat_semibold, FontWeight.W600)
)

private val Domine = FontFamily(
    Font(R.font.domine_regular),
    Font(R.font.domine_bold, FontWeight.Bold)
)

Agora, crie um objeto Typography que um MaterialTheme aceite, especificando os TextStyles para cada estilo semântico na escala:

val JetnewsTypography = Typography(
    h4 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W600,
        fontSize = 30.sp
    ),
    h5 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W600,
        fontSize = 24.sp
    ),
    h6 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W600,
        fontSize = 20.sp
    ),
    subtitle1 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W600,
        fontSize = 16.sp
    ),
    subtitle2 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W500,
        fontSize = 14.sp
    ),
    body1 = TextStyle(
        fontFamily = Domine,
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp
    ),
    body2 = TextStyle(
        fontFamily = Montserrat,
        fontSize = 14.sp
    ),
    button = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W500,
        fontSize = 14.sp
    ),
    caption = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.Normal,
        fontSize = 12.sp
    ),
    overline = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W500,
        fontSize = 12.sp
    )
)

Abra o Theme.kt e atualize o elemento que pode ser composto JetnewsTheme para usar nossa nova Typography:

@Composable
fun JetnewsTheme(content: @Composable () -> Unit) {
  MaterialTheme(
    colors = LightColors,
+   typography = JetnewsTypography,
    content = content
  )
}

Abra Home.kt e atualize a visualização para ver a nova tipografia.

Formas

Queremos usar formas para expressar nossa marca no app e usar o formato de canto cortado em vários elementos:

ebcdf2fb3364f0d3.png

O Compose oferece as classes RoundedCornerShape e CutCornerShape, que podem ser usadas para definir o tema de formas.

Crie um novo arquivo Shape.kt no pacote theme e adicione o código abaixo:

val JetnewsShapes = Shapes(
    small = CutCornerShape(topStart = 8.dp),
    medium = CutCornerShape(topStart = 24.dp),
    large = RoundedCornerShape(8.dp)
)

Abra o Theme.kt e atualize o elemento que pode ser composto JetnewsTheme para usar essas Shapes:

@Composable
fun JetnewsTheme(content: @Composable () -> Unit) {
  MaterialTheme(
    colors = LightColors,
    typography = JetnewsTypography,
+   shapes = JetnewsShapes,
    content = content
  )
}

Abra o Home.kt e atualize a visualização para ver como o Card que mostra a postagem em destaque reflete o tema de formas recém-aplicado.

Tema escuro

Oferecer suporte a um tema escuro no app não só ajuda o app a se integrar melhor aos dispositivos dos usuários, que têm um tema escuro global no Android 10 e versões mais recentes, mas também pode reduzir o uso de energia e oferecer suporte às necessidades de acessibilidade. O Material Design oferece uma orientação de design (em inglês) sobre como criar um tema escuro. Veja uma paleta de cores alternativa que gostaríamos de implementar para o tema escuro:

2523f19026837a19.png

Abra Color.kt e adicione as cores abaixo:

val Red200 = Color(0xfff297a2)
val Red300 = Color(0xffea6d7e)

Agora, abra Theme.kt e adicione:

private val DarkColors = darkColors(
    primary = Red300,
    primaryVariant = Red700,
    onPrimary = Color.Black,
    secondary = Red300,
    onSecondary = Color.Black,
    error = Red200
)

Depois, atualize o JetnewsTheme:

@Composable
fun JetnewsTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
  content: @Composable () -> Unit
) {
  MaterialTheme(
+   colors = if (darkTheme) DarkColors else LightColors,
    typography = JetnewsTypography,
    shapes = JetnewsShapes,
    content = content
  )
}

Aqui, adicionamos um novo parâmetro para usar um tema escuro e o padronizamos para consultar a configuração global do dispositivo. Esse é um bom padrão, mas ainda é fácil de substituir se você quiser que uma determinada tela sempre/nunca seja escura ou criar uma @Preview com tema escuro.

Abra o Home.kt e crie uma nova visualização do elemento que pode ser composto FeaturedPost para mostrar o tema escuro:

@Preview("Featured Post • Dark")
@Composable
private fun FeaturedPostDarkPreview() {
    val post = remember { PostRepo.getFeaturedPost() }
    JetnewsTheme(darkTheme = true) {
        FeaturedPost(post = post)
    }
}

Atualize o painel de visualização para ver o tema escuro.

84f93b209ce4fd46.png

5. Como trabalhar com cores

Na última etapa, vimos como criar um tema próprio para definir cores, estilos de tipo e formas no app. Todos os componentes do Material Design usam essas personalizações por padrão. Por exemplo, o elemento FloatingActionButton que pode ser composto usa a cor secondary do tema por padrão, mas é possível definir uma cor alternativa especificando um valor diferente para esse parâmetro:

@Composable
fun FloatingActionButton(
  backgroundColor: Color = MaterialTheme.colors.secondary,
  ...
) {

Você nem sempre vai querer usar as configurações padrão. Esta seção mostra como trabalhar com cores no app.

Cores brutas

Como vimos anteriormente, o Compose oferece uma classe Color. É possível criar as cores localmente, armazená-las em um object etc.:

Surface(color = Color.LightGray) {
  Text(
    text = "Hard coded colors don't respond to theme changes :(",
    textColor = Color(0xffff00ff)
  )
}

A Color tem vários métodos úteis, como copy, que permite criar uma nova cor com diferentes valores alfa/vermelho/verde/azul.

Cores do tema

Uma abordagem mais flexível é aproveitar cores do seu tema:

Surface(color = MaterialTheme.colors.primary)

Neste caso, estamos usando o object do MaterialTheme em que a propriedade colors retorna as Colors definidas no elemento MaterialTheme que pode ser composto. Isso significa que podemos oferecer suporte a diferentes aparências apenas fornecendo conjuntos de cores diferentes para o nosso tema, sem precisar tocar no código do aplicativo. Por exemplo, nossa AppBar usa a cor primary, e o plano de fundo da tela usa surface. A mudança de cores do tema é refletida nesses elementos que podem ser compostos:

e042098f0d035638.png

3dfb0d1134625efc.png

Como cada cor no nosso tema é uma instância de Color, também podemos derivar cores facilmente usando o método copy:

val derivedColor = MaterialTheme.colors.onSurface.copy(alpha = 0.1f)

No exemplo acima, fizemos uma cópia da cor onSurface, mas com 10% de opacidade. Essa abordagem garante que as cores funcionem com temas diferentes, em vez de precisar codificar cores estáticas.

Cores de superfície e conteúdo

Muitos componentes aceitam um par de cores e de "cores de conteúdo":

Surface(
  color: Color = MaterialTheme.colors.surface,
  contentColor: Color = contentColorFor(color),
  ...

TopAppBar(
  backgroundColor: Color = MaterialTheme.colors.primarySurface,
  contentColor: Color = contentColorFor(backgroundColor),
  ...

Isso permite não apenas definir a cor de um elemento que pode ser composto, mas também fornecer uma cor padrão para o "conteúdo", ou seja, elementos que podem ser compostos contidos nele. Muitos elementos que podem ser compostos usam essa cor para o conteúdo por padrão, como a cor do Text ou a tonalidade do Icon. O método contentColorFor extrai a cor "on" adequada para todas as cores do tema. Por exemplo, se você definir um plano de fundo primary, ele vai retornar onPrimary como a cor do conteúdo. Se você definir uma cor que não seja do tema para o plano de fundo, vai precisar fornecer uma cor para o conteúdo adequada.

Surface(color = MaterialTheme.colors.primary) {
  Text(...) // default text color is 'onPrimary'
}
Surface(color = MaterialTheme.colors.error) {
  Icon(...) // default tint is 'onError'
}

Você pode usar LocalContentColor e CompositionLocal para extrair a cor que contrasta com o plano de fundo atual:

BottomNavigationItem(
  unselectedContentColor = LocalContentColor.current ...

Ao definir a cor de qualquer elemento, prefira usar uma Surface para isso, porque ela define uma cor para o conteúdo com um valor CompositionLocal adequado. Cuidado com chamadas diretas de Modifier.background que não definem uma cor adequada.

-Row(Modifier.background(MaterialTheme.colors.primary)) {
+Surface(color = MaterialTheme.colors.primary) {
+  Row(
...

No momento, os componentes Header sempre têm um plano de fundo Color.LightGray. Isso pode parecer normal em um tema claro, mas tem alto contraste em relação ao plano de fundo no tema escuro. Eles também não definem a cor específica do texto, mas herdam a cor atual do conteúdo, que pode não corresponder ao plano de fundo:

f80956b5e213d88e.png

Vamos corrigir isso. No elemento que pode ser composto Header em Home.kt, remova o modificador background especificando a cor codificada. Em vez disso, envolva o Text em uma Surface com uma cor derivada do tema e especifique que o conteúdo precisa ter a cor primary:

+ Surface(
+   color = MaterialTheme.colors.onSurface.copy(alpha = 0.1f),
+   contentColor = MaterialTheme.colors.primary,
+   modifier = modifier
+ ) {
  Text(
    text = text,
    modifier = Modifier
      .fillMaxWidth()
-     .background(Color.LightGray)
      .padding(horizontal = 16.dp, vertical = 8.dp)
  )
+ }

Alfa do conteúdo

Com frequência, queremos enfatizar ou não o conteúdo para comunicar a importância e fornecer uma hierarquia visual. O Material Design recomenda (link em inglês) o uso de diferentes níveis de opacidade para transmitir esses níveis de importância variados.

Isso é implementado pelo Jetpack Compose via LocalContentAlpha. É possível especificar um Alfa de conteúdo para uma hierarquia fornecendo um valor para esse CompositionLocal. Os elementos filhos que podem ser compostos podem usar esse valor. Por exemplo: Text e Icon, por padrão, usam a combinação de LocalContentColor ajustada para usar LocalContentAlpha. O Material Design especifica alguns valores Alfa padrão (high, medium e disabled), que são modelados pelo objeto ContentAlpha. Observe que MaterialTheme define LocalContentAlpha como ContentAlpha.high por padrão.

// By default, both Icon & Text use the combination of LocalContentColor &
// LocalContentAlpha. De-emphasize content by setting a different content alpha
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
    Text(...)
}
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
    Icon(...)
    Text(...)
}

Isso torna mais fácil e consistente representar a importância dos componentes.

Usaremos o Alfa de conteúdo para esclarecer a hierarquia de informações da postagem em destaque. Em Home.kt, no elemento PostMetadata que pode ser composto, dê ênfase aos metadados medium:

+ CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
  Text(
    text = text,
    modifier = modifier
  )
+ }

5f24fbfac3932c26.png

Tema escuro

Como vimos anteriormente, para implementar temas escuros no Compose, basta fornecer diferentes conjuntos de cores e consultas no tema. Algumas exceções são:

É possível confirmar se você está executando um tema claro:

val isLightTheme = MaterialTheme.colors.isLight

Esse valor é definido pelas funções do builder lightColors e darkColors.

Nos modos escuros do Material Design, as superfícies com elevações mais altas recebem sobreposições de elevação (link em inglês) e o plano de fundo é clareado. Isso é implementado automaticamente ao usar uma paleta de cores escuras:

Surface(
  elevation = 2.dp,
  color = MaterialTheme.colors.surface, // color will be adjusted for elevation
  ...

Podemos ver esse comportamento automático no nosso app nos componentes TopAppBar e Card que estamos usando. Por padrão, eles têm elevações de 4 dp e 1 dp. Portanto, o plano de fundo é clareado automaticamente no tema escuro para representar melhor essa elevação:

cd3869170d47055e.png

O Material Design sugere que você faça isso, evitando áreas grandes de cores brilhantes no tema escuro. Um padrão comum é colorir a cor primary de um contêiner no tema claro e a cor surface em temas escuros. Muitos componentes usam essa estratégia por padrão, por exemplo, barras de apps e navegação inferior. Para facilitar a implementação, a classe Colors oferece uma cor da primarySurface que fornece exatamente esse comportamento, e esses componentes são usados por padrão.

No momento, o app define a cor da barra de apps como primary. Podemos seguir essa orientação mudando a cor para primarySurface ou removendo esse parâmetro já que ele é o padrão. Na AppBar que pode ser composta, mude o parâmetro backgroundColor da TopAppBar:

@Composable
private fun AppBar() {
  TopAppBar(
    ...
-   backgroundColor = MaterialTheme.colors.primary
+   backgroundColor = MaterialTheme.colors.primarySurface
  )
}

6. Como trabalhar com texto

Ao trabalhar com texto, usamos o elemento que pode ser composto Text para exibir texto, o TextField e o OutlinedTextField para entradas de texto e o TextStyle para aplicar um único estilo ao texto. Podemos usar as AnnotatedStrings para definir vários estilos a textos.

Como vimos no caso das cores, os componentes do Material Design que exibem texto vão usar nossas personalizações de tipografia no tema:

Button(...) {
  Text("This text will use MaterialTheme.typography.button style by default")
}

Conseguir isso é um pouco mais complexo do que usar parâmetros padrão, como vimos no caso das cores. Isso ocorre porque os componentes não costumam exibir texto, mas oferecem "APIs de slot", o que possibilita transmitir um elemento Text que pode ser composto. Então, como os componentes definem um estilo de tipografia no tema? Internamente, eles usam o elemento ProvideTextStyle que pode ser composto (que usa um CompositionLocal) para definir um TextStyle "atual". O elemento que pode ser composto Text volta para o padrão de consultar o estilo "atual" se você não fornecer um parâmetro textStyle concreto.

Veja como exemplo as classes Button e Text do Compose:

@Composable
fun Button(
    // many other parameters
    content: @Composable RowScope.() -> Unit
) {
  ...
  ProvideTextStyle(MaterialTheme.typography.button) { //set the "current" text style
    ...
    content()
  }
}

@Composable
fun Text(
    // many, many parameters
    style: TextStyle = LocalTextStyle.current // get the value set by ProvideTextStyle
) { ...

Estilos de texto do tema

Assim como com as cores, é melhor extrair os TextStyles do tema atual, o que incentiva você a usar um conjunto pequeno e consistente de estilos e os torna mais fáceis de manter. A MaterialTheme.typography extrai a instância Typography definida no elemento MaterialTheme, permitindo que você use os estilos definidos:

Text(
  style = MaterialTheme.typography.subtitle2
)

Caso você precise personalizar um TextStyle, é possível copy (copiar) e substituir as propriedades (que são apenas uma data class) ou usar o elemento Text que pode ser composto e que aceita diversos parâmetros de estilo que vão sobrepor qualquer TextStyle:

Text(
  text = "Hello World",
  style = MaterialTheme.typography.body1.copy(
    background = MaterialTheme.colors.secondary
  )
)
Text(
  text = "Hello World",
  style = MaterialTheme.typography.subtitle2,
  fontSize = 22.sp // explicit size overrides the size in the style
)

Muitos lugares no app aplicam automaticamente os TextStyles do tema. Por exemplo, os estilos da TopAppBar usam o title como h6 e o ListItem define o estilo do texto principal e secundário como subtitle1 e body2, respectivamente.

Vamos aplicar os estilos tipográficos do tema ao restante do app. Defina o Header para usar subtitle2 e o texto da FeaturedPost para usar h6 como o título e body2 para o autor e os metadados:

@Composable
fun Header(...) {
  ...
  Text(
    text = text,
+   style = MaterialTheme.typography.subtitle2

33df571f8ff85c7f.png

Vários estilos

Se você precisar aplicar vários estilos a um texto, use a classe AnnotatedString para aplicar a marcação, adicionando SpanStyles aos intervalos no texto. Você pode adicionar isso dinamicamente ou usar a sintaxe DSL para criar conteúdo:

val text = buildAnnotatedString {
  append("This is some unstyled text\n")
  withStyle(SpanStyle(color = Color.Red)) {
    append("Red text\n")
  }
  withStyle(SpanStyle(fontSize = 24.sp)) {
    append("Large text")
  }
}

Vamos estilizar as tags que descrevem cada postagem no nosso app. Atualmente, elas usam o mesmo estilo de texto que o restante dos metadados. Usaremos o estilo de texto overline e uma cor de plano de fundo para diferenciar os dois. No elemento PostMetadata que pode ser composto:

+ val tagStyle = MaterialTheme.typography.overline.toSpanStyle().copy(
+   background = MaterialTheme.colors.primary.copy(alpha = 0.1f)
+ )
post.tags.forEachIndexed { index, tag ->
  ...
+ withStyle(tagStyle) {
    append(" ${tag.toUpperCase()} ")
+ }
}

95e883150e70b346.png

7. Como trabalhar com formas

Assim como a cor e a tipografia, a definição do tema da forma vai ser refletida nos componentes do Material Design. Por exemplo, os Buttons vão usar a forma definida para componentes pequenos:

@Composable
fun Button( ...
  shape: Shape = MaterialTheme.shapes.small
) {

Assim como as cores, os componentes do Material Design usam parâmetros padrão para que seja simples confirmar qual categoria de forma um componente vai usar ou para fornecer uma alternativa. Para mapear totalmente os componentes para a categoria de forma, consulte a documentação (em inglês).

Alguns componentes usam formas do tema modificadas de acordo com o contexto. Um exemplo é o TextField, que usa por padrão o tema de forma pequena, mas aplica um tamanho de canto zero aos cantos de baixo:

@Composable
fun FilledTextField(
  // other parameters
  shape: Shape = MaterialTheme.shapes.small.copy(
    bottomStart = ZeroCornerSize, // overrides small theme style
    bottomEnd = ZeroCornerSize // overrides small theme style
  )
) {

62de8f5fd0a5fae9.png

Formas do tema

É possível usar formas ao criar seus próprios componentes com elementos que podem ser compostos ou Modifiers que aceitam formas. Por exemplo, Surface, Modifier.clip, Modifier.background, Modifier.border e assim por diante.

@Composable
fun UserProfile(
  ...
  shape: Shape = MaterialTheme.shapes.medium
) {
  Surface(shape = shape) {
    ...
  }
}

Vamos adicionar formas às imagens exibidas no PostItem e aplicar a forma small do tema com um clip Modifier para cortar o canto esquerdo de cima:

@Composable
fun PostItem(...) {
  ...
  Image(
    painter = painterResource(post.imageThumbId),
+   modifier = Modifier.clip(shape = MaterialTheme.shapes.small)
  )

dd3c6c8a4a4917ff.png

8. "Estilos" de componentes

O Compose não oferece uma maneira explícita de extrair o estilo de um componente, como os estilos CSS ou da visualização do Android. Como todos os componentes do Compose são criados no Kotlin, há outras maneiras de atingir o mesmo objetivo. Em vez disso, crie sua própria biblioteca de componentes personalizados para usar em todo o app.

Já fizemos isso no nosso app:

@Composable
fun Header(
  text: String,
  modifier: Modifier = Modifier
) {
  Surface(
    color = MaterialTheme.colors.onSurface.copy(alpha = 0.1f),
    contentColor = MaterialTheme.colors.primary,
    modifier = modifier.semantics { heading() }
  ) {
    Text(
      text = text,
      style = MaterialTheme.typography.subtitle2,
      modifier = Modifier
        .fillMaxWidth()
        .padding(horizontal = 16.dp, vertical = 8.dp)
    )
  }
}

O elemento Header é essencialmente um Text estilizado que pode ser usado em todo o app.

Todos os componentes são feitos de elementos de níveis mais baixos. Use esses mesmos elementos para personalizar os componentes do Material Design. Por exemplo, vimos que o Button usa o elemento ProvideTextStyle para definir um estilo de texto padrão para o conteúdo transmitido a ele. Você pode usar exatamente o mesmo mecanismo para definir seu próprio estilo de texto:

@Composable
fun LoginButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Button(
        colors = ButtonConstants.defaultButtonColors(
            backgroundColor = MaterialTheme.colors.secondary
        ),
        onClick = onClick,
        modifier = modifier
    ) {
        ProvideTextStyle(...) { // set our own text style
            content()
        }
    }
}

Neste exemplo, criamos nosso próprio "estilo" do LoginButton ao combinar a classe Button padrão e especificar certas propriedades, como uma backgroundColor e estilo de texto diferentes.

Também não há um conceito de estilo padrão, ou seja, uma maneira padrão de personalizar a aparência de um tipo de componente. Mais uma vez, é possível fazer isso criando seu próprio componente que envolve e personaliza um componente da biblioteca. Digamos que você queira personalizar a forma de todos os Buttons do app, mas não queira mudar o tema de formato pequeno, o que afetaria outros componentes que não são do Button. Para fazer isso, crie seu próprio elemento que pode ser composto e use-o em todo o app:

@Composable
fun AcmeButton(
  // expose Button params consumers should be able to change
) {
  val acmeButtonShape: Shape = ...
  Button(
    shape = acmeButtonShape,
    // other params
  )
}

9. Parabéns

Parabéns! Você concluiu este codelab e estilizou um app do Jetpack Compose.

Você implementou um tema do Material Design, personalizando a cor, a tipografia e as formas usadas em todo o app para expressar sua marca e promover consistência. Você adicionou suporte a temas claros e escuros.

A seguir

Confira os outros codelabs no programa de aprendizagem do Compose:

Leia mais

Apps de exemplo

  • Owl demonstrando vários temas
  • Jetcaster demonstrando temas dinâmicos
  • Jetsnack demonstrando a implementação de um sistema de design personalizado

Documentos de referência