Componentes y diseños de Material

Jetpack Compose ofrece una implementación de Material Design, un sistema de diseño integral para crear interfaces digitales. Los componentes de Material (botones, tarjetas, interruptores, etc.) y los diseños como Scaffold están disponibles como funciones que admiten composición.

Los componentes de Material son bloques de compilación interactivos para crear una interfaz de usuario. Compose ofrece varios de estos componentes listos para usar. Para ver cuáles están disponibles, consulta la referencia de la API de Compose Material.

Los componentes de Material usan valores que proporciona un MaterialTheme en tu app:

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

Para obtener más información sobre los temas, consulta la Guía de temas para Compose.

Espacios de contenido

Los componentes de Material que admiten contenido interno (como etiquetas de texto, íconos, etc.) tienden a ofrecer "espacios" (lambdas genéricas que aceptan contenido que admite composición), además de constantes públicas, como tamaño y relleno, para admitir la disposición del contenido interno de modo que coincida con las especificaciones de Material.

Un ejemplo de esto es 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: Un Button que usa el espacio content y el relleno predeterminado (izquierda), y un Button que utiliza el espacio content para proporcionar un contentPadding personalizado (derecha).

Button tiene un espacio lambda final genérico content, que usa un RowScope para diseñar elementos de contenido que admitan composición en una fila. También tiene un parámetro contentPadding para aplicar relleno al contenido interno. Puedes usar constantes proporcionadas a través de ButtonDefaults o valores personalizados.

Otro ejemplo es ExtendedFloatingActionButton:

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

Figura 2: Un ExtendedFloatingActionButton con los espacios icon y text.

En lugar de una lambda genérica content, ExtendedFloatingActionButton tiene dos espacios para una etiqueta icon y otra text. Si bien cada espacio puede incluir contenido genérico que admite composición, el componente considera cómo se disponen esos fragmentos internos. Controla el relleno, la alineación y el tamaño de forma interna.

Scaffold

Compose proporciona diseños convenientes para combinar componentes de Material en patrones de pantalla comunes. Los elementos que admiten composición, como Scaffold, proporcionan espacios para varios componentes y otros elementos de pantalla.

Contenido de la pantalla

Scaffold tiene un espacio lambda final genérico: content. La lambda recibe una instancia de PaddingValues que se debe aplicar a la raíz del contenido (por ejemplo, a través de Modifier.padding), para desplazar las barras inferior y superior, si es que las hay.

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

Barras de la app

Scaffold proporciona espacios para una barra de la app superior o inferior. La ubicación de los elementos que admiten composición se controla de forma interna.

Puedes usar el espacio topBar y una TopAppBar:

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

O bien utilizar el espacio bottomBar y una BottomAppBar:

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

Estos espacios se pueden usar para otros componentes de Material, como BottomNavigation. También puedes utilizar elementos que admiten composición personalizados. Por ejemplo, consulta la pantalla de incorporación del ejemplo de Owl.

Botones de acción flotantes

Scaffold proporciona un espacio para un botón de acción flotante.

Puedes usar el espacio floatingActionButton y un FloatingActionButton:

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

La posición inferior del BAF que admite composición se controla de forma interna. Puedes usar el parámetro floatingActionButtonPosition para ajustar la posición horizontal:

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

Si usas el espacio bottomBar del elemento que admite composición Scaffold, puedes usar el parámetro isFloatingActionButtonDocked para superponer el BAF con la barra de la app inferior:

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

Figura 3: Un Scaffold que usa los espacios floatingActionButton y bottomBar. El parámetro isFloatingActionButtonDocked se establece en false (parte superior) y true (parte inferior).

BottomAppBar admite cortes de BAF con el parámetro cutoutShape, que acepta cualquier Shape. Se recomienda proporcionar la misma Shape que usa el componente conectado. Por ejemplo, FloatingActionButton usa MaterialTheme.shapes.small con un tamaño de esquina del 50% como valor predeterminado para su 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: Un Scaffold con una BottomAppBar y un FloatingActionButton anclado. BottomAppBar tiene una cutoutShape personalizada que coincide con la Shape que usa el FloatingActionButton.

Barras de notificaciones

Scaffold proporciona un medio para mostrar barras de notificaciones.

Esto se proporciona a través de ScaffoldState, que incluye una propiedad SnackbarHostState. Puedes usar rememberScaffoldState para crear una instancia de ScaffoldState que se debe pasar a Scaffold con el parámetro scaffoldState. SnackbarHostState proporciona acceso a la función showSnackbar. Esta función de suspensión requiere un CoroutineScope (por ejemplo, mediante rememberCoroutineScope), y se la puede llamar en respuesta a eventos de IU para mostrar una Snackbar en 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
}

Puedes proporcionar una acción opcional y ajustar la duración de Snackbar. La función snackbarHostState.showSnackbar acepta parámetros actionLabel y duration adicionales, y muestra un 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
}

Puedes proporcionar una Snackbar personalizada con el parámetro snackbarHost. Consulta SnackbarHost API reference docs para obtener más información.

Paneles laterales

Scaffold proporciona un espacio para un panel lateral de navegación modal. El diseño y la hoja arrastable del elemento que admite composición se controla de forma interna.

Puedes usar el espacio drawerContent, que usa un ColumnScope, para disponer en una columna elementos que admiten composición con contenido de paneles laterales:

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

Scaffold acepta una cantidad de parámetros adicionales de panel lateral. Por ejemplo, puedes activar o desactivar si el panel lateral responde a arrastres con el parámetro drawerGesturesEnabled:

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

La apertura y el cierre del panel lateral de manera programática se realiza mediante ScaffoldState, que incluye una propiedad DrawerState que se debe pasar a Scaffold con el parámetro scaffoldState. DrawerState proporciona acceso a las funciones open y close, así como a propiedades relacionadas con el estado actual del panel lateral. Estas funciones de suspensión requieren un CoroutineScope (por ejemplo, mediante rememberCoroutineScope), y se lo puede llamar en respuesta a eventos de 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
}

Si quieres implementar un panel lateral de navegación modal sin un Scaffold, puedes usar el elemento que admite composición ModalDrawer. Acepta parámetros de panel lateral similares a Scaffold.

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

Si quieres implementar un panel lateral de navegación inferior, puedes usar el elemento que admite composición BottomDrawer:

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

Hojas inferiores

Si quieres implementar una hoja inferior estándar, puedes usar el elemento que admite composición BottomSheetScaffold. Acepta parámetros similares a Scaffold, como topBar, floatingActionButton y snackbarHost. Incluye parámetros adicionales que proporcionan un medio para mostrar hojas inferiores.

Puedes usar el espacio sheetContent, que usa un ColumnScope, para disponer en una columna elementos que admiten composición con contenido de hojas:

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

BottomSheetScaffold acepta una cantidad de parámetros adicionales de hoja. Por ejemplo, puedes establecer la altura de la hoja con el parámetro sheetPeekHeight. También puedes activar o desactivar si la hoja responde a los arrastres con el parámetro sheetGesturesEnabled.

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

) {
    // Screen content
}

La expansión y contracción de la hoja de forma programática se realiza a través de BottomSheetScaffoldState, que incluye una propiedad BottomSheetState. Puedes usar rememberBottomSheetScaffoldState para crear una instancia de BottomSheetScaffoldState que se debe pasar a BottomSheetScaffold con el parámetro scaffoldState. BottomSheetState proporciona acceso a las funciones expand y collapse, así como a propiedades relacionadas con el estado actual de la hoja. Estas funciones de suspensión requieren un CoroutineScope (por ejemplo, mediante rememberCoroutineScope), y se lo puede llamar en respuesta a eventos de 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
}

Si quieres implementar una hoja inferior modal, puedes usar el elemento que admite composición ModalBottomSheetLayout:

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

Fondo

Si quieres implementar un fondo, puedes usar el elemento que admite composición BackdropScaffold.

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

BackdropScaffold acepta una serie de parámetros de fondo adicionales. Por ejemplo, puedes configurar la altura del aviso de la capa posterior y la altura inactiva mínima de la capa frontal con los parámetros peekHeight y headerHeight. También puedes activar o desactivar si el fondo responde a los arrastres con el 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
)

Se puede revelar y ocultar el fondo de manera programática mediante BackdropScaffoldState. Puedes usar rememberBackdropScaffoldState para crear una instancia de BackdropScaffoldState que se debe pasar a BackdropScaffold con el parámetro scaffoldState. BackdropScaffoldState proporciona acceso a las funciones reveal y conceal, así como a propiedades relacionadas con el estado actual del fondo. Estas funciones de suspensión requieren un CoroutineScope (por ejemplo, mediante rememberCoroutineScope), y se lo puede llamar en respuesta a eventos de 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
    }
)