Las Chips son elementos de selección que permiten introducir información, realizar selecciones, filtrar contenido o ejecutar acciones.
En cuanto a su anatomía, la imagen anterior muestra los elementos usados en su construcción
- Leading icon
- Label
- Trailing icon
Además, se resalta el hecho de que existen cuatro tipos: Assist Chip, Filter Chip, Input Chip y Suggestion Chip. Tipos que en este tutorial verás como implementar.
Veamos.
Proyecto De Chips En Android Studio
1. Antes de iniciar asegúrate de crear un nuevo proyecto en Android Studio con el nombre de «Chips». Usa la plantilla de tipo Jetpack Compose para facilitar la creación de los elementos básicos de nuestra UI.
2. Luego crea un archivo llamado ChipsScreen.kt
, el cual contendrá el esqueleto general para ir añadiendo nuestros ejemplos:
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
@Composable
fun ChipsScreen() {
Scaffold(
topBar = { TopBar() },
content = { padding -> MainContent(padding) }
)
}
@Preview
@Composable
private fun Preview() {
ChipsTheme {
ChipsScreen()
}
}
3. El contenido principal será una columna con todos los elementos asociados a cada ejemplo. Elementos que irán siendo creados a medida que avancemos en el tutorial:
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun MainContent(padding: PaddingValues) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(16.dp)
.verticalScroll(rememberScrollState())
) {
AssistChips(modifier = Modifier.weight(1f))
HorizontalDivider()
FilterChips(modifier = Modifier.weight(1f))
HorizontalDivider()
InputChips(modifier = Modifier.weight(1f))
HorizontalDivider()
SuggestionChips(modifier = Modifier.weight(1f))
}
}
4. Finaliza llamando la función principal desde MainActivity
:
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ChipsTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
ChipsScreen()
}
}
}
}
}
Puedes ver el código final directamente en el repositorio de GitHub:
Con esta preparación, ya podemos iniciar con el primer tipo de chip, Assist Chip.
Assist Chip
Las Assist Chips o Chips de Asistencia representan un grupo de acciones relacionadas al contexto de un contenido. En Jetpack Compose se define por AssistChip()
:
@Composable
fun AssistChip(
onClick: () -> Unit,
label: @Composable () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
leadingIcon: (@Composable () -> Unit)? = null,
trailingIcon: (@Composable () -> Unit)? = null,
shape: Shape = AssistChipDefaults.shape,
colors: ChipColors = AssistChipDefaults.assistChipColors(),
elevation: ChipElevation? = AssistChipDefaults.assistChipElevation(),
border: ChipBorder? = AssistChipDefaults.assistChipBorder(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
): Unit
¿Qué atributos usaremos en este tutorial?:
onClick
: Tipo función que se ejecuta al hacer click en la chiplabel
: Texto en la chipleadingIcon
: Icono al inicio de la chiptrailingIcon
: Icono al final de la chip
Ejemplo:
La ilustración anterior muestra el primer ejemplo que veremos: un grupo de acciones asociadas a comunicación de información (Llamada, Mensaje y Videollamada). Al presionarlas procesaremos el click asociado y mostraremos la etiqueta asociada con la acción.
Veamos como implementarlo.
Crear Estado De Assist Chips
Lo primero que haremos será representar los datos visto en el diseño del ejemplo como clases de datos. Sus propiedades son:
- Última acción ejecutada
- Lista de acciones (que a su vez contiene a su evento de click, etiqueta e icono)
Con esto en mente, crea el archivo AssistChipsState.kt
y añade las siguientes clases:
import androidx.compose.ui.graphics.vector.ImageVector
data class AssistChipsState(
val executedAction: String,
val actions: List<AssistChipState>
) {
companion object {
val Default = AssistChipsState("Ninguna", emptyList())
}
}
data class AssistChipState(
val onClick: () -> Unit,
val label: String,
val icon: ImageVector
) {
init {
require(label.length <= 20)
}
}
Crear Assist Chips ViewModel
Ahora, sostendremos el estado y procesaremos los eventos de click en un viewmodel llamado AssistChipsViewModel
.
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Message
import androidx.compose.material.icons.filled.Call
import androidx.compose.material.icons.filled.Videocam
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
class AssistChipsViewModel : ViewModel() {
private val executedAction = MutableStateFlow("Ninguna")
private val actions = listOf(
AssistChipState(
onClick = { onActionClick("Llamada") },
label = "Llamada",
icon = Icons.Default.Call
),
AssistChipState(
onClick = { onActionClick("Mensaje") },
label = "Mensaje",
icon = Icons.AutoMirrored.Default.Message
),
AssistChipState(
onClick = { onActionClick("Videollamada") },
label = "Videollamada",
icon = Icons.Default.Videocam
)
)
val state = executedAction.map { action ->
AssistChipsState(executedAction = action, actions = actions)
}
private fun onActionClick(action: String) {
executedAction.value = action
}
}
Del código anterior:
executedAction
: Es un flujo de estado mutable que almacena el último valor de la acción ejecutadaactions
: Lista de lectura para dibujar las chips en pantalla y redirigir los clicks aonActionClick()
state
: Es el estado general de UI, actualizado pormap()
cuando la acción ejecutada es modificadaonActionClick()
: Función que asigna el nombre de la acción ejecutada al estado respectivo
Crear Assist Chips
Como tercer paso, crearemos las funciones @Composable
para materializar nuestra UI.
1. Así que añade el archivo Kotlin AssistChips.kt
y define la función principal AssistChips()
:
@Composable
fun AssistChips(
modifier: Modifier,
viewModel: AssistChipsViewModel = viewModel()
) {
val state by viewModel.state.collectAsState(initial = AssistChipsState.Default)
AssistChips(modifier, state)
}
Como ves, su firma recibe al viewmodel y extrae su estado para delegarlo a otra función encargada de la estructuración.
2. La segunda función AssistChips()
toma al estado para invocar un elemento Column()
y organizar el diseño:
@Composable
private fun AssistChips(
modifier: Modifier = Modifier,
state: AssistChipsState
) {
Column(modifier = modifier) {
Text("Assist Chip", style = MaterialTheme.typography.titleMedium)
Spacer(Modifier.size(8.dp))
AssistChipGroup(state.actions)
Text("Chip accionada: ${state.executedAction}")
}
}
3. La función AssistChipGroup()
toma el estado de las acciones e itera sobre ellas para dibujar una chip por cada una:
@Composable
private fun AssistChipGroup(
chipStates: List<AssistChipState>
) {
Row(
modifier = Modifier.horizontalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
chipStates.forEach { chipState ->
AssistChip(state = chipState)
}
}
}
4. Nuestra función AssistChip()
toma el estado e invoca a la función de Compose con cada propiedad que definimos:
@Composable
fun AssistChip(state: AssistChipState) {
AssistChip(
onClick = state.onClick,
label = { Text(state.label) },
leadingIcon = {
Icon(
imageVector = state.icon,
contentDescription = state.label,
modifier = Modifier.size(AssistChipDefaults.IconSize)
)
}
)
}
Ejecuta el proyecto Android Studio y verás el siguiente resultado:
Filter Chip
Las Filter Chips representan etiquetas o descripciones que filtran una colección al ser seleccionadas. Son una alternativa a los toggle buttons o checkboxes.
Usaremos la función FilterChip()
de Compose para crearlas en pantalla, la cual posee casi los mismos atributos de AssistChip()
, pero también un parámetro selected
para determinar si esta seleccionada o no.
Ejemplo:
Generar un grupo de chips para filtrar una lista de zapatos según la disponibilidad de sus tallas. Cada que se seleccione una chip, los zapatos irán actualizándose para coincidir con el criterio.
Iniciemos con la solución.
Fuente De Datos
Claramente, por cuestiones de practicidad, el origen de los zapatos será una fuente de datos en memoria que contiene la lista de los productos y las tallas disponibles.
En primer lugar, crearemos la entidad para los zapatos, la cual posee el nombre y las tallas en las que está disponible:
data class Shoe(
val name: String,
val availableSizes: Set<String>
)
Y luego crearemos la declaración de objeto MemoryShoes
que contendrá los datos a usar en nuestro ejemplo:
object MemoryShoes {
val sizes = setOf("US 6", "US 6.5", "US 7")
private val shoes = listOf(
Shoe(name = "Nizza Platform", availableSizes = setOf("US 6", "US 6.5")),
Shoe(name = "Racer TR23", availableSizes = setOf("US 6.5")),
Shoe(name = "Duramo SL Running", availableSizes = setOf("US 6")),
Shoe(name = "Samba OG", availableSizes = setOf("US 7")),
Shoe(name = "Daily 3.0", availableSizes = setOf("US 6", "US 6.5", "US 7"))
)
fun findBy(selectedSizes: Set<String>): List<Shoe> {
if (selectedSizes.isEmpty())
return shoes
return shoes.filter { shoe ->
selectedSizes.all { selectedSize ->
selectedSize in shoe.availableSizes
}
}
}
}
Observa que sizes
representa las tallas actuales en la fuente de datos.
Y el método findBy()
nos permitirá filtrar (filter()
+ all()
) los zapatos cuyas tallas disponibles coincidan con los filtros seleccionados en la UI.
Estado De Filter Chips
El estado de la pantalla contiene dos informaciones: los filtros y la lista de zapatos. Por lo que definimos eso en una clase de datos llamada FilterChipsState
:
data class FilterChipsState(
val shoes: String,
val shoeFilter: List<FilterChipState>
) {
companion object {
val Default = FilterChipsState("...", emptyList())
}
}
data class FilterChipState(
val onClick: () -> Unit,
val label: String,
val selected: Boolean
)
Crear ViewModel Para Filter Chips
Lo siguiente es crear la nueva clase FilterChipsViewModel
:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.develou.chips.filterchips.data.MemoryShoes
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
class FilterChipsViewModel : ViewModel() {
private val selectedSizes = MutableStateFlow(emptySet<String>())
val state = selectedSizes.map { selectedSizes ->
FilterChipsState(
MemoryShoes.findBy(selectedSizes).asUi(),
mapSizesToFilterState(selectedSizes)
)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = FilterChipsState.Default
)
private fun mapSizesToFilterState(selectedSizes: Set<String>): List<FilterChipState> {
return MemoryShoes.sizes.map { size ->
FilterChipState(
onClick = { updateSelectedSizes(size) },
label = size,
selected = size in selectedSizes
)
}
}
private fun updateSelectedSizes(clickedSize: String) {
if (clickedSize in selectedSizes.value) {
selectedSizes.update { it - clickedSize }
} else {
selectedSizes.update { it + clickedSize }
}
}
}
Donde:
selectedSizes
: Es el estado mutable para las tallas seleccionadas en el filtrostate
: Es el resultado de mapear el cambio de los filtros seleccionados y consultar aMemoryShoes
con el valor actualmapSizesToFilterState()
: Función extraída para convertir la lista de tallas en elementosFilterChipState
updateSelectedSizes()
: Función extraída para actualizar los filtros seleccionados. Se invoca enonClick
de cada filtro
Crear Filter Chips
Crea el archivo FilterChips.kt
y añade cuatro funciones componibles como las que definimos en el ejemplo anterior:
@Composable
fun FilterChips(
modifier: Modifier,
viewModel: FilterChipsViewModel = viewModel()
) {
val state by viewModel.state.collectAsState()
FilterChips(state, modifier)
}
@Composable
private fun FilterChips(
state: FilterChipsState,
modifier: Modifier
) {
Column(modifier = modifier.padding(top = 8.dp)) {
Text("Filter Chip", style = MaterialTheme.typography.titleMedium)
FilterChipsGroup(state.shoeFilter)
Text(state.shoes)
}
}
@Composable
private fun FilterChipsGroup(chips: List<FilterChipState>) {
Row(
modifier = Modifier.horizontalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
chips.forEach { chip ->
FilterChip(chip)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FilterChip(state: FilterChipState) {
FilterChip(
selected = state.selected,
onClick = state.onClick,
label = { Text(state.label) },
leadingIcon = if (state.selected) {
{
Icon(
imageVector = Icons.Default.Done,
contentDescription = "Seleccionado",
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
}
} else {
null
}
)
}
La definición para este ejemplo es similar al de las Assist Chips, la diferencia radica en la construcción de la chip. Donde leadingIcon
usó la propiedad FilterChipState.selected
para generar el Icono inicial.
Si la chip está seleccionada, dibujamos un icono con una marca de check (Icons.Default.Done
), de lo contrario pasamos null
al parámetro.
Puedes comprobar el filtrado corriendo el aplicativo y seleccionando las chips de filtro:
Input Chip
Las Input Chips representan información confirmada por el usuario en un campo de texto. Es decir, si el texto introducido cumple con los criterios de selección, es convertido en una pieza accionable de información.
En Jetpack Compose es representada por la función InputChip()
. Al igual que FilterChip()
, recibe los componentes de anatomía y el estado de selección.
Ejemplo:
Seguiremos el ejemplo de la documentación oficial, donde se tiene un campo de texto que recibe los ingredientes extras en la preparación de una Pizza.
Por practicidad, convertiremos el texto en una Input Chip cuando se presione el botón de acción del teclado virtual. Procedamos a implementar esta idea.
Crear Estado De Input Chips
El estado de la pantalla se compone del texto que escribimos y de las conversiones a chips que se hacen. Crea el archivo InputChipsState.kt
y represéntalos así:
data class InputChipsState(
val input: String,
val confirmedInputs: Map<String, ConfirmedInput>,
val onInputChange: (String) -> Unit,
val onConfirmInput: () -> Unit,
val onBackspaceClick: () -> Unit
) {
val noInput get() = input.isBlank()
val chipList get() = confirmedInputs.values
companion object {
const val InputNone = ""
val Default = InputChipsState(InputNone, emptyMap(), {}, {}) {}
}
}
data class ConfirmedInput(
val label: String,
val isSelected: Boolean,
val onClick: () -> Unit,
val onRemove: () -> Unit
)
De la clase InputChipsState
:
input
: El texto de entradaconfirmedInputs
: Las entradas convertidas en chips, claseConfirmedInput
.label
: EtiquetaisSelected
: Estado de selecciónonClick
: Acción de click. No usada en este ejemploonRemove
: Acción de click en icono de eliminación
onInputChange
: La acción cuando se cambia el textoonConfirmInput
: La acción cuando se confirma el textoonBackspaceClick
: La acción cuando se presiona la barra de retroceso sin texto de entrada presente
Crear ViewModel De Input Chips
Añade un nuevo viewmodel llamado InputChipsViewModel
para añadir el estado y la respuesta a eventos como sigue:
class InputChipsViewModel : ViewModel() {
private val _state = MutableStateFlow(
InputChipsState(
input = InputNone,
confirmedInputs = emptyMap(),
onInputChange = ::onInputChange,
onConfirmInput = ::onConfirmInput,
onBackspaceClick = ::onBackspaceClick
)
)
val state = _state.asStateFlow()
private fun onInputChange(input: String) {
_state.update { currentState ->
currentState.copy(input = input.trim())
}
}
private fun onConfirmInput() {
if (_state.value.input.isBlank()) return
_state.update { currentState ->
val newChips = buildChip(
chips = currentState.confirmedInputs,
label = currentState.input,
isSelected = false
)
currentState.copy(
confirmedInputs = newChips,
input = InputNone
)
}
}
private fun onBackspaceClick() {
val chips = state.value.confirmedInputs.values
if (chips.isEmpty() || state.value.input.isNotBlank()) return
val lastChip = chips.last()
if (lastChip.isSelected) {
removeChip(lastChip.label)
} else {
selectChip(lastChip.label)
}
}
private fun selectChip(label: String) {
_state.update { currentState ->
currentState.copy(
confirmedInputs = buildChip(
chips = currentState.confirmedInputs,
label = label,
isSelected = true
)
)
}
}
private fun buildChip(
chips: Map<String, ConfirmedInput>,
label: String,
isSelected: Boolean
): Map<String, ConfirmedInput> {
val newPickedExtra = ConfirmedInput(
label = label,
isSelected = isSelected,
onClick = {},
onRemove = { removeChip(label) }
)
return chips + (label to newPickedExtra)
}
private fun removeChip(chipToRemoveKey: String) {
_state.update { currentState ->
val newChips = currentState.confirmedInputs - chipToRemoveKey
currentState.copy(confirmedInputs = newChips)
}
}
}
Comprendamos el código:
state
: Es el flujo de lectura que usará nuestra función de UI para poblar la interfaz y delegar eventosonInputChange()
: Actualizamos el valor de la entradaonConfirmInput()
: Creamos una nueva chip (buildChip()
) y la añadimos al estadoonBackspaceClick()
: Removemos una chip (removeChip()
) si ya está seleccionada, de lo contrario la seleccionamos (selectChip()
)
Con el estado sostenido en el viewmodel, pasemos a crear la interfaz.
Crear Input Chips
1. Al igual que los anteriores ejemplos, la estructura general del diseño es una columna con un título y el contenido por debajo. Por lo que en un nuevo archivo Kotlin llamado InputChips.kt
creamos las funciones generales InputChips()
como sigue:
@Composable
fun InputChips(
modifier: Modifier = Modifier,
viewModel: InputChipsViewModel = viewModel()
) {
val state by viewModel.state.collectAsState()
InputChips(modifier = modifier, state = state)
}
@Composable
private fun InputChips(
modifier: Modifier,
state: InputChipsState
) {
Column(modifier = modifier) {
SmallSpace()
Text("Input Chip", style = MaterialTheme.typography.titleMedium)
SmallSpace()
InputChipTextField(state)
}
}
2. El área fuerte del código se encuentra en la función InputChipTextField()
, donde debemos crear una combinación entre un Text Field e Input Chips para materializar la conversión de chips.
@Composable
private fun InputChipTextField(state: InputChipsState) {
val keyboard = LocalSoftwareKeyboardController.current
val scrollState = rememberScrollState()
val scope = rememberCoroutineScope()
val focusRequester = remember { FocusRequester() }
Row(
Modifier
.border(2.dp, Color.Gray, RoundedCornerShape(4.dp))
.fillMaxWidth()
.height(56.dp)
.horizontalScroll(scrollState)
.onGloballyPositioned { // (1)
scope.launch { scrollState.scrollTo(it.size.width) }
}
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() },
onClick = { // (2)
focusRequester.requestFocus()
keyboard?.show()
}
),
verticalAlignment = Alignment.CenterVertically
) {
InputChipsGroup(state.chipList) // (3)
GeneralTextField( // (4)
state = state,
focusRequester = focusRequester,
keyboard = keyboard
)
}
}
Como ves, el padre del componente es un Row
, el cual distribuye las chips y el campo de texto. Algunos puntos a tener en cuenta:
- Usaremos el modificador
onGloballyPositioned()
para scrollear horizontalmente la fila cuando el ancho cambie - Hacer clic en la fila solicitará el foco para el campo de texto y abrirá el teclado virtual
- Usaremos la función
InputChipsGroup()
para mostrar las chips - La función
GeneralTextField()
se encargará del campo de texto
3. Creemos el grupo de input chips como una fila de invocaciones de InputChip()
:
@Composable
private fun InputChipsGroup(
chips: Collection<ConfirmedInput>
) {
if (chips.isEmpty()) return
Row(
modifier = Modifier.padding(start = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
chips.forEach { pickedExtra ->
InputChip(state = pickedExtra)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun InputChip(state: ConfirmedInput) {
InputChip(
selected = state.isSelected,
onClick = state.onClick,
label = { Text(state.label) },
trailingIcon = {
Icon(
Icons.Default.Close,
contentDescription = "Remover",
Modifier
.clickable(onClick = state.onRemove)
.size(InputChipDefaults.IconSize),
)
}
)
}
4. Para el campo de texto crearemos la función GeneralTextField()
, la cual se compone de la siguiente manera:
@Composable
private fun GeneralTextField(
state: InputChipsState,
focusRequester: FocusRequester,
keyboard: SoftwareKeyboardController?
) {
Box {
Placeholder(state)
SpecificTextField(
state,
focusRequester,
keyboard
)
}
}
@Composable
private fun BoxScope.Placeholder(state: InputChipsState) {
if (state.noInput && state.confirmedInputs.isEmpty()) {
Text(
text = "Ingredientes",
modifier = Modifier.Companion
.align(Alignment.CenterStart)
.padding(start = 16.dp),
color = Color.Black.copy(alpha = 0.66f)
)
}
}
@Composable
private fun SpecificTextField(
state: InputChipsState,
focusRequester: FocusRequester,
keyboard: SoftwareKeyboardController?
) {
OutlinedTextField(
modifier = Modifier
.requiredWidth(inputWidth(state.input))
.onKeyEvent {
if (it.key == Key.Backspace) {
state.onBackspaceClick()
}
false
}
.focusRequester(focusRequester),
value = state.input,
onValueChange = state.onInputChange,
singleLine = true,
keyboardActions = KeyboardActions(
onDone = {
state.onConfirmInput()
if (state.noInput) keyboard?.hide()
}
),
keyboardOptions= KeyboardOptions(capitalization = KeyboardCapitalization.Sentences),
colors = OutlinedTextFieldDefaults.colors(
unfocusedBorderColor = Color.Transparent,
focusedBorderColor = Color.Transparent
)
)
}
@Composable
private fun inputWidth(state: String): Dp {
val inputOrOneCharacter = state.ifEmpty { " " }
return with(LocalDensity.current) {
val inputWidth = ParagraphIntrinsics(
text = inputOrOneCharacter,
style = MaterialTheme.typography.bodyLarge,
density = this,
fontFamilyResolver = LocalFontFamilyResolver.current
).maxIntrinsicWidth
inputWidth.toDp() + 32.dp
}
}
Del código anterior tenemos que:
Placeholder()
: Es el texto que indica al usuario el contenido del campo de textoSpecificTextField()
: Es la invocación directa de unOutlinedTextField()
. En su interior delegamos las propiedades del estado de UIinputWidth()
: Produce el ancho actual del campo de texto basado en la entrada actual. Esto se logra con la utilidadParagraphIntrinsics()
, la cual mide el ancho máxima y mínimo de un texto
Termina este ejemplo ejecutando el proyecto. Si todo sale bien, podrás convertir tu texto en chips:
Suggestion Chip
Las Suggestion Chips actúan como sugerencias asociadas a la intención del usuario en una característica de la App. Al igual que en la ilustración anterior, uno de sus usos más comunes es en las recomendaciones para enviar mensajes a un chat o servicio de asistencia. También son útiles en casos de uso de recomendaciones de búsqueda.
Las construiremos con la función SuggestionChip() de Compose como veremos en el siguiente ejemplo.
Ejemplo:
Simularemos una app de Chat que nos sugiere tres tipos de saludos populares al conversar. Cada uno será representado por una Suggestion Chip; y en el momento que sean clicados, mostraremos su etiqueta como el mensaje enviado.
Manos a la obra.
Crear Estado De Suggestion Chips
El estado de este ejemplo se compone de las sugerencias de saludo, el saludo enviado y la visibilidad del panel de los saludos. Así que crea un archivo llamado SuggestionChipsState.kt
y materializa esta definición:
data class SuggestionChipsState(
val suggestions: List<SuggestionState>,
val greeting: String,
val isPanelVisible: Boolean
) {
companion object {
val Default = SuggestionChipsState(
suggestions = emptyList(),
greeting = "",
isPanelVisible = false
)
}
}
data class SuggestionState(
val onClick: () -> Unit,
val label: String
)
Crear ViewModel Para Suggestion Chips
Ahora crea la clase SuggestionChipsViewModel
y declara una instancia del estado declarado previamente:
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf
class SuggestionChipsViewModel : ViewModel() {
private val isGreetingPanelVisible = MutableStateFlow(true)
private val sentGreeting = MutableStateFlow("")
private val greetingSuggestions = flowOf(listOf("Hola", "Buenos días", "¿Qué tal?"))
val state = combine(
isGreetingPanelVisible,
sentGreeting,
greetingSuggestions
) { isVisible, greeting, suggestions ->
SuggestionChipsState(
buildSuggestionStates(suggestions),
greeting,
isVisible
)
}
private fun buildSuggestionStates(
suggestions: List<String>
): List<SuggestionState> {
return suggestions.map { suggestion ->
SuggestionState(
onClick = {
isGreetingPanelVisible.value = false
sentGreeting.value = suggestion
},
label = suggestion
)
}
}
}
Si te fijas, state
es la combinación de tres flujos relacionados al estado general. En la lambda de transformación creamos una instancia de SuggestionChipsState()
con los nuevos valores que se vayan emitiendo.
La función buildSuggestionStates()
crea elementos SuggestionState
a partir de la lista de strings de sugerencias. Y por supuesto, en el argumento de onClick
se ocultará el panel y se actualizará el saludo enviado.
Crear Suggestion Chips
Finaliza creando el archivo SuggestionChips.kt
con las funciones de UI SuggestionChips()
que acomodarán al contenido. Como son elementos distribuidos verticalmente, usamos a Column
como lo hicimos en los ejemplos anteriores.
@Composable
fun SuggestionChips(
modifier: Modifier = Modifier,
viewModel: SuggestionChipsViewModel = viewModel()
) {
val state by viewModel.state.collectAsState(SuggestionChipsState.Default)
SuggestionChips(modifier, state)
}
@Composable
private fun SuggestionChips(
modifier: Modifier,
state: SuggestionChipsState
) {
Column(modifier = modifier) {
SmallSpace()
Text("Suggestion Chip", style = MaterialTheme.typography.titleMedium)
SmallSpace()
Text("Chat: ${state.greeting}")
if (!state.isPanelVisible) return
SuggestionChipsGroup(state.suggestions)
}
}
Fíjate que si isPanelVisible
es false
, entonces evitamos el dibujado de las chips.
La creación de las suggestion chips va en la función SuggestionChipsGroups()
. Y como es sabido, es la iteración sobre la lista de chips del estado:
@Composable
private fun SuggestionChipsGroup(chips: List<SuggestionState>) {
Row(
modifier = Modifier.horizontalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
chips.forEach {
SuggestionChip(it)
}
}
}
@Composable
private fun SuggestionChip(chipState: SuggestionState) {
SuggestionChip(
onClick = chipState.onClick,
label = { Text(chipState.label) }
)
}
Una vez creada la interfaz gráfica, pasamos a ejecutar el aplicativo, donde el resultado será la siguiente interacción: