Camadas da arquitetura do Jetpack Compose

Esta página oferece uma visão geral de alto nível das camadas arquitetônicas que compõem o Jetpack Compose e os princípios básicos que fundamentam esse design.

O Jetpack Compose não é um projeto monolítico. Ele é criado com vários módulos que são agrupados para formar uma pilha completa. Ao entender os diferentes módulos que compõem o Jetpack Compose, você pode:

  • usar o nível de abstração adequado para criar seu app ou sua biblioteca;
  • saber quando é possível "descer" para um nível inferior para ter mais controle ou personalização;
  • minimizar suas dependências.

Camadas

As principais camadas do Jetpack Compose são estas:

Figura 1. As principais camadas do Jetpack Compose.

Cada camada é criada com base nos níveis inferiores, combinando funcionalidades para criar componentes de nível superior. Cada uma delas se baseia em APIs públicas das camadas inferiores para verificar os limites do módulo e possibilitar que você substitua qualquer camada, se necessário. Vamos examinar essas camadas de baixo para cima.

Runtime
Este módulo fornece os princípios básicos do ambiente de execução do Compose, como remember, mutableStateOf, a anotação @Composable e SideEffect. Você pode criar diretamente nessa camada se precisar apenas dos recursos de gerenciamento de árvore do Compose, não da IU dele.
UI
A camada UI é composta por vários módulos, como ui-text, ui-graphics, ui-tooling etc. Esses módulos implementam os princípios básicos do kit de ferramentas de IU, como LayoutNode, Modifier, gerenciadores de entrada, layouts personalizados e desenho. Você pode criar nessa camada se precisar apenas dos conceitos fundamentais de um kit de ferramentas de IU.
Foundation
Este módulo fornece elementos básicos independentes do sistema de design para a IU do Compose, como Row e Column, LazyColumn, o reconhecimento de gestos específicos etc. Você pode desenvolver na camada Foundation para criar seu próprio sistema de design.
Material
Este módulo oferece uma implementação do sistema Material Design para a IU do Compose, fornecendo um sistema de temas, componentes estilizados, indicações de ondulação e ícones. Crie nessa camada ao usar o Material Design no seu app.

Princípios de design

Um princípio orientador do Jetpack Compose é fornecer recursos pequenos e com foco específico, que podem ser agrupados (ou compostos), em vez de apenas alguns componentes monolíticos. Essa abordagem tem várias vantagens.

Controle

Componentes de nível superior tendem a fazer mais por você, mas limitam a quantidade de controle direto que você tem. Se você precisar de mais controle, use um "menu suspenso" para usar um componente de nível inferior.

Por exemplo, para animar a cor de um componente, use a API animateColorAsState:

val color = animateColorAsState(if (condition) Color.Green else Color.Red)

No entanto, se você precisava que o componente sempre começasse cinza, não será possível fazer isso com essa API. Em vez disso, você pode usar o menu suspenso para usar a API Animatable de nível inferior:

val color = remember { Animatable(Color.Gray) }
LaunchedEffect(condition) {
    color.animateTo(if (condition) Color.Green else Color.Red)
}

A API animateColorAsState de nível superior é criada com base na API Animatable de nível inferior. O uso da API de nível inferior é mais complexo, mas oferece mais controle. Escolha o nível de abstração que melhor atende às suas necessidades.

Personalização

Criar componentes de nível superior usando elementos menores facilita muito a personalização de componentes, caso ela seja necessária. Por exemplo, considere a implementação do Button fornecida pela camada do Material Design:

@Composable
fun Button(
    // …
    content: @Composable RowScope.() -> Unit
) {
    Surface(/* … */) {
        CompositionLocalProvider(/* … */) { // set LocalContentAlpha
            ProvideTextStyle(MaterialTheme.typography.button) {
                Row(
                    // …
                    content = content
                )
            }
        }
    }
}

Um Button é criado usando quatro componentes:

  1. Um Surface do Material Design, que define plano de fundo, forma, gerenciamento de cliques etc.

  2. Um CompositionLocalProvider, que muda a versão Alfa do conteúdo quando o botão é ativado ou desativado.

  3. Um ProvideTextStyle, que define o estilo de texto padrão a ser usado.

  4. Uma Row, que fornece a política de layout padrão para o conteúdo do botão.

Omitimos alguns parâmetros e comentários para deixar a estrutura mais clara, mas o componente inteiro tem apenas 40 linhas de código, porque ele apenas combina esses quatro componentes para implementar o botão. Componentes como o Button são rigorosos quanto aos parâmetros que eles expõem, equilibrando a possibilidade de personalizações comuns com o excesso de parâmetros, que pode dificultar o uso de um componente. Os componentes do Material Design, por exemplo, oferecem personalizações especificadas no sistema do Material Design, facilitando o cumprimento dos princípios desse sistema.

No entanto, se você quiser fazer uma personalização além dos parâmetros de um componente, poderá "diminuir" um nível e bifurcar um componente. Por exemplo, o Material Design especifica que os botões precisam ter um plano de fundo de cor sólida. Se você precisar de um plano de fundo em gradiente, essa opção não é compatível com os parâmetros do Button. Nesse caso, você pode usar a implementação do Button do Material como referência e criar seu próprio componente:

@Composable
fun GradientButton(
    // …
    background: List<Color>,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Row(
        // …
        modifier = modifier
            .clickable(onClick = {})
            .background(
                Brush.horizontalGradient(background)
            )
    ) {
        CompositionLocalProvider(/* … */) { // set material LocalContentAlpha
            ProvideTextStyle(MaterialTheme.typography.button) {
                content()
            }
        }
    }
}

A implementação acima continua usando componentes da camada do Material Design, como os conceitos do conteúdo Alfa atual (link em inglês) e o estilo de texto atual. No entanto, ele substitui o Surface do Material Design por um Row e o define para alcançar a aparência desejada.

Se você não quiser usar os conceitos do Material Design, por exemplo, ao criar seu próprio sistema de design personalizado, use apenas os componentes da camada de base:

@Composable
fun BespokeButton(
    // …
    backgroundColor: Color,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Row(
        // …
        modifier = modifier
            .clickable(onClick = {})
            .background(backgroundColor)
    ) {
        // No Material components used
        content()
    }
}

O Jetpack Compose reserva os nomes mais simples para os componentes de nível mais alto. Por exemplo, androidx.compose.material.Text é baseado no androidx.compose.foundation.text.BasicText. Isso possibilita fornecer sua implementação com o nome mais detectável possível, caso você queira substituir níveis mais altos.

Escolher a abstração certa

A filosofia do Compose de criação de componentes reutilizáveis em camadas significa que você nem sempre precisa recorrer aos elementos básicos de nível mais baixo. Muitos componentes de nível superior não só oferecem mais funcionalidades, como geralmente também implementam práticas recomendadas, como compatibilidade com acessibilidade.

Por exemplo, se você quiser adicionar suporte a gestos ao seu componente personalizado, pode criar isso do zero usando o Modifier.pointerInput. Mas há outros componentes de nível superior criados com base nele que podem oferecer um ponto de partida melhor, como o Modifier.draggable, o Modifier.scrollable ou o Modifier.swipeable.

Como regra, prefira criar com base no componente de nível mais alto que ofereça a funcionalidade necessária para você se beneficiar das práticas recomendadas incluídas.

Saiba mais

Consulte o exemplo do Jetsnack para ver um exemplo de como criar um sistema de design personalizado.