Componentes e layouts do Material Design

O Jetpack Compose oferece uma implementação do Material Design, um sistema de design abrangente para criar interfaces digitais. Componentes do Material Design (botões, cards, chaves etc.) e layouts como o Scaffold estão disponíveis como funções que podem ser compostas.

Os componentes do Material Design (link em inglês) são elementos básicos para a criação de uma interface do usuário. O Compose oferece vários componentes prontos para uso. Para ver quais estão disponíveis, confira a referência da API Compose Material.

Os componentes do Material Design usam os valores fornecidos por um MaterialTheme no seu app:

@Composable
fun MyApp() {
    MaterialTheme {
        // Material Components like Button, Card, Switch, etc.
    }
}

Para saber mais sobre os temas, consulte o Guia de temas do Compose.

Slots de conteúdo

Os componentes do Material Design compatíveis com conteúdo interno (rótulos de texto, ícones etc.) tendem a oferecer "slots", lambdas genéricos que aceitam conteúdo que pode ser composto, bem como constantes públicas, como tamanho e padding, para oferecer suporte à criação do layout do conteúdo interno para corresponder às especificações do Material Design.

Um exemplo disso é o Button:

Button(
    onClick = { /* ... */ },
    // Uses ButtonDefaults.ContentPadding by default
    contentPadding = PaddingValues(
        start = 20.dp,
        top = 12.dp,
        end = 20.dp,
        bottom = 12.dp
    )
) {
    // Inner content including an icon and a text label
    Icon(
        Icons.Filled.Favorite,
        contentDescription = "Favorite",
        modifier = Modifier.size(ButtonDefaults.IconSize)
    )
    Spacer(Modifier.size(ButtonDefaults.IconSpacing))
    Text("Like")
}

Figura 1. Um Button usando o slot content e o padding padrão (à esquerda) e um Button usando o slot content que fornece um contentPadding personalizado (à direita).

Button tem um slot lambda final de content genérico, que usa um RowScope para definir o layout dos conteúdos que podem ser compostos em uma linha. Ele também tem um parâmetro contentPadding para aplicar o padding ao conteúdo interno. É possível usar constantes fornecidas pelos ButtonDefaults ou valores personalizados.

Outro exemplo é o ExtendedFloatingActionButton:

ExtendedFloatingActionButton(
    onClick = { /* ... */ },
    icon = {
        Icon(
            Icons.Filled.Favorite,
            contentDescription = "Favorite"
        )
    },
    text = { Text("Like") }
)

Figura 2. Um ExtendedFloatingActionButton usando os slots icon e text.

Em vez de um lambda final de content genérico, ExtendedFloatingActionButton tem dois slots para um icon e um rótulo de text. Embora cada slot ofereça suporte para conteúdo genérico que pode ser composto, o componente é rigoroso sobre como esse conteúdo interno é exibido. Ele processa o padding, o alinhamento e o tamanho internamente.

Scaffold

O Compose oferece layouts convenientes para combinar componentes do Material Design em padrões de tela comuns. Funções que podem ser compostas, como Scaffold, fornecem slots para vários componentes e outros elementos de tela.

Conteúdo da tela

Scaffold tem um slot lambda final de content genérico. O lambda recebe uma instância de PaddingValues que precisa ser aplicada à raiz do conteúdo, por exemplo, via Modifier.padding, para compensar as partes superior e inferior das barras, se elas existirem.

Scaffold(/* ... */) { contentPadding ->
    // Screen content
    Box(modifier = Modifier.padding(contentPadding)) { /* ... */ }
}

Barras de apps

Scaffold fornece slots para uma barra de apps na parte superior ou na parte inferior. A posição das funções que podem ser compostas é processada internamente.

É possível usar o slot topBar e uma TopAppBar:

Scaffold(
    topBar = {
        TopAppBar { /* Top app bar content */ }
    }
) {
    // Screen content
}

É possível usar o slot bottomBar e uma BottomAppBar:

Scaffold(
    bottomBar = {
        BottomAppBar { /* Bottom app bar content */ }
    }
) {
    // Screen content
}

Esses slots podem ser usados para outros componentes do Material Design, como a BottomNavigation. Também é possível usar elementos personalizados que podem ser compostos. Por exemplo, veja a tela de integração da amostra do Owl.

Botões de ação flutuantes

Scaffold fornece um slot para um botão de ação flutuante.

É possível usar o slot floatingActionButton e um FloatingActionButton:

Scaffold(
    floatingActionButton = {
        FloatingActionButton(onClick = { /* ... */ }) {
            /* FAB content */
        }
    }
) {
    // Screen content
}

A posição inferior do FAB que pode ser composto é processada internamente. É possível usar o parâmetro floatingActionButtonPosition para ajustar a posição horizontal:

Scaffold(
    floatingActionButton = {
        FloatingActionButton(onClick = { /* ... */ }) {
            /* FAB content */
        }
    },
    // Defaults to FabPosition.End
    floatingActionButtonPosition = FabPosition.Center
) {
    // Screen content
}

Se você estiver usando o slot bottomBar do Scaffold que pode ser composto, será possível usar o parâmetro isFloatingActionButtonDocked para sobrepor o FAB com a barra de apps inferior:

Scaffold(
    floatingActionButton = {
        FloatingActionButton(onClick = { /* ... */ }) {
            /* FAB content */
        }
    },
    // Defaults to false
    isFloatingActionButtonDocked = true,
    bottomBar = {
        BottomAppBar { /* Bottom app bar content */ }
    }
) {
    // Screen content
}

Figura 3. Um Scaffold usando os slots floatingActionButton e bottomBar. O parâmetro isFloatingActionButtonDocked está definido como false (parte superior) e true (parte inferior).

BottomAppBar oferece suporte a cortes no FAB com o parâmetro cutoutShape, que aceita qualquer Shape. É recomendável fornecer o mesmo Shape usado pelo componente fixado. Por exemplo, FloatingActionButton usa MaterialTheme.shapes.small com um tamanho de canto de 50% como valor padrão para o parâmetro shape:

Scaffold(
    floatingActionButton = {
        FloatingActionButton(onClick = { /* ... */ }) {
            /* FAB content */
        }
    },
    isFloatingActionButtonDocked = true,
    bottomBar = {
        BottomAppBar(
            // Defaults to null, that is, No cutout
            cutoutShape = MaterialTheme.shapes.small.copy(
                CornerSize(percent = 50)
            )
        ) {
            /* Bottom app bar content */
        }
    }
) {
  // Screen content
}

Figura 4. Um Scaffold com uma BottomAppBar e um FloatingActionButton na base. A BottomAppBar tem uma cutoutShape personalizada que corresponde ao Shape usado pelo FloatingActionButton.

Snackbars

O Scaffold fornece uma maneira de exibir snackbars.

Isso é fornecido pelo ScaffoldState, que inclui uma propriedade SnackbarHostState. É possível usar rememberScaffoldState para criar uma instância do ScaffoldState que precisa ser transmitida para o Scaffold com o parâmetro scaffoldState. SnackbarHostState fornece acesso à função showSnackbar. Essa função de suspensão exige um CoroutineScope, por exemplo, usando rememberCoroutineScope, e pode ser chamada em resposta a eventos da IU para mostrar uma Snackbar em um Scaffold.

val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
Scaffold(
    scaffoldState = scaffoldState,
    floatingActionButton = {
        ExtendedFloatingActionButton(
            text = { Text("Show snackbar") },
            onClick = {
                scope.launch {
                    scaffoldState.snackbarHostState
                        .showSnackbar("Snackbar")
                }
            }
        )
    }
) {
    // Screen content
}

Você pode fornecer uma ação opcional e ajustar a duração da Snackbar. A função snackbarHostState.showSnackbar aceita outros parâmetros actionLabel e duration e retorna um SnackbarResult.

val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
Scaffold(
    scaffoldState = scaffoldState,
    floatingActionButton = {
        ExtendedFloatingActionButton(
            text = { Text("Show snackbar") },
            onClick = {
                scope.launch {
                    val result = scaffoldState.snackbarHostState
                        .showSnackbar(
                            message = "Snackbar",
                            actionLabel = "Action",
                            // Defaults to SnackbarDuration.Short
                            duration = SnackbarDuration.Indefinite
                        )
                    when (result) {
                        SnackbarResult.ActionPerformed -> {
                            /* Handle snackbar action performed */
                        }
                        SnackbarResult.Dismissed -> {
                            /* Handle snackbar dismissed */
                        }
                    }
                }
            }
        )
    }
) {
    // Screen content
}

É possível fornecer uma Snackbar personalizada com o parâmetro snackbarHost. Consulte SnackbarHost API reference docs para ver mais informações.

Gavetas

Scaffold fornece um slot para uma gaveta modal de navegação. A página e o layout arrastáveis do elemento que pode ser composto são processados internamente.

Você pode usar o slot drawerContent, que usa um ColumnScope para definir o layout dos conteúdos de gaveta que podem ser compostos em uma coluna:

Scaffold(
    drawerContent = {
        Text("Drawer title", modifier = Modifier.padding(16.dp))
        Divider()
        // Drawer items
    }
) {
    // Screen content
}

Scaffold aceita vários outros parâmetros de gaveta. Por exemplo, é possível ativar ou desativar a resposta da gaveta às ações de arrastar com o parâmetro drawerGesturesEnabled:

Scaffold(
    drawerContent = {
        // Drawer content
    },
    // Defaults to true
    drawerGesturesEnabled = false
) {
    // Screen content
}

A abertura e o fechamento da gaveta são feitos de forma programática pelo ScaffoldState, que inclui uma propriedade do DrawerState que precisa ser transmitida para o Scaffold com o parâmetro scaffoldState. DrawerState fornece acesso às funções open e close, além de propriedades relacionadas ao estado atual da gaveta. Essas funções de suspensão exigem um CoroutineScope, por exemplo, usando rememberCoroutineScope, e podem ser chamadas em resposta a eventos da IU.

val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
Scaffold(
    scaffoldState = scaffoldState,
    drawerContent = {
        // Drawer content
    },
    floatingActionButton = {
        ExtendedFloatingActionButton(
            text = { Text("Open or close drawer") },
            onClick = {
                scope.launch {
                    scaffoldState.drawerState.apply {
                        if (isClosed) open() else close()
                    }
                }
            }
        )
    }
) {
    // Screen content
}

Caso você queira implementar uma gaveta de navegação modal sem um Scaffold, use a ModalDrawer que pode ser composta. Ela aceita parâmetros de gaveta parecidos com o Scaffold.

val drawerState = rememberDrawerState(DrawerValue.Closed)
ModalDrawer(
    drawerState = drawerState,
    drawerContent = {
        // Drawer content
    }
) {
    // Screen content
}

Se você quiser implementar uma gaveta de navegação inferior, use a BottomDrawer que pode ser composta:

val drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
BottomDrawer(
    drawerState = drawerState,
    drawerContent = {
        // Drawer content
    }
) {
    // Screen content
}

Páginas inferiores

Caso você queira implementar uma página inferior padrão, use o elemento BottomSheetScaffold que pode ser composto. Ele aceita parâmetros parecidos para Scaffold, como topBar, floatingActionButton e snackbarHost, além de incluir outros parâmetros que fornecem uma maneira de exibir as páginas inferiores.

Você pode usar o slot sheetContent, que usa um ColumnScope para definir o layout dos conteúdos da página que pode ser compostos em uma coluna:

BottomSheetScaffold(
    sheetContent = {
        // Sheet content
    }
) {
    // Screen content
}

BottomSheetScaffold aceita vários outros parâmetros de página. Por exemplo, você pode definir a altura máxima da página com o parâmetro sheetPeekHeight. Também pode ativar ou desativar a resposta da gaveta às ações de arrastar com o parâmetro sheetGesturesEnabled.

BottomSheetScaffold(
    sheetContent = {
        // Sheet content
    },
    // Defaults to BottomSheetScaffoldDefaults.SheetPeekHeight
    sheetPeekHeight = 128.dp,
    // Defaults to true
    sheetGesturesEnabled = false

) {
    // Screen content
}

A expansão e o recolhimento da página de forma programática são feitos pelo BottomSheetScaffoldState, que inclui uma propriedade BottomSheetState. É possível usar rememberBottomSheetScaffoldState para criar uma instância do BottomSheetScaffoldState que precisa ser transmitida para o BottomSheetScaffold com o parâmetro scaffoldState. BottomSheetState fornece acesso às funções expand e collapse, além de propriedades relacionadas ao estado da página. Essas funções de suspensão exigem um CoroutineScope, por exemplo, usando rememberCoroutineScope, e podem ser chamadas em resposta a eventos da IU.

val scaffoldState = rememberBottomSheetScaffoldState()
val scope = rememberCoroutineScope()
BottomSheetScaffold(
    scaffoldState = scaffoldState,
    sheetContent = {
        // Sheet content
    },
    floatingActionButton = {
        ExtendedFloatingActionButton(
            text = { Text("Expand or collapse sheet") },
            onClick = {
                scope.launch {
                    scaffoldState.bottomSheetState.apply {
                        if (isCollapsed) expand() else collapse()
                    }
                }
            }
        )
    }
) {
    // Screen content
}

Caso você queira implementar uma página inferior modal, use o elemento ModalBottomSheetLayout que pode ser composto:

val sheetState = rememberModalBottomSheetState(
    ModalBottomSheetValue.Hidden
)
ModalBottomSheetLayout(
    sheetState = sheetState,
    sheetContent = {
        // Sheet content
    }
) {
    // Screen content
}

Pano de fundo

Caso você queira implementar um pano de fundo, é possível usar o elemento que pode ser composto BackdropScaffold.

BackdropScaffold(
    appBar = {
        // Top app bar
    },
    backLayerContent = {
        // Back layer content
    },
    frontLayerContent = {
        // Front layer content
    }
)

BackdropScaffold aceita vários outros parâmetros de pano de fundo. Por exemplo, você pode definir a altura máxima da camada de fundo e a altura mínima inativa da camada em primeiro plano com os parâmetros peekHeight e headerHeight. Também é possível ativar ou desativar a resposta do pano de fundo às ações de arrastar com o parâmetro gesturesEnabled.

BackdropScaffold(
    appBar = {
        // Top app bar
    },
    backLayerContent = {
        // Back layer content
    },
    frontLayerContent = {
        // Front layer content
    },
    // Defaults to BackdropScaffoldDefaults.PeekHeight
    peekHeight = 40.dp,
    // Defaults to BackdropScaffoldDefaults.HeaderHeight
    headerHeight = 60.dp,
    // Defaults to true
    gesturesEnabled = false
)

A revelação e a ocultação de panos de fundo de maneira programática são feitas pelo BackdropScaffoldState. É possível usar rememberBackdropScaffoldState para criar uma instância do BackdropScaffoldState que precisa ser transmitida para o BackdropScaffold com o parâmetro scaffoldState. BackdropScaffoldState fornece acesso às funções reveal e conceal, além de propriedades relacionadas ao estado do pano de fundo. Essas funções de suspensão exigem um CoroutineScope, por exemplo, usando rememberCoroutineScope, e podem ser chamadas em resposta a eventos da IU.

val scaffoldState = rememberBackdropScaffoldState(
    BackdropValue.Concealed
)
val scope = rememberCoroutineScope()
BackdropScaffold(
    scaffoldState = scaffoldState,
    appBar = {
        TopAppBar(
            title = { Text("Backdrop") },
            navigationIcon = {
                if (scaffoldState.isConcealed) {
                    IconButton(
                        onClick = {
                            scope.launch { scaffoldState.reveal() }
                        }
                    ) {
                        Icon(
                            Icons.Default.Menu,
                            contentDescription = "Menu"
                        )
                    }
                } else {
                    IconButton(
                        onClick = {
                            scope.launch { scaffoldState.conceal() }
                        }
                    ) {
                        Icon(
                            Icons.Default.Close,
                            contentDescription = "Close"
                        )
                    }
                }
            },
            elevation = 0.dp,
            backgroundColor = Color.Transparent
        )
    },
    backLayerContent = {
        // Back layer content
    },
    frontLayerContent = {
        // Front layer content
    }
)