Chips En Android

Las Chips son elementos de selección que permiten introducir información, realizar selecciones, filtrar contenido o ejecutar acciones.

Anatomía de Chips
Anatomía de Chips

En cuanto a su anatomía, la imagen anterior muestra los elementos usados en su construcción

  1. Leading icon
  2. Label
  3. 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

Ejemplo de Assist Chips
Ejemplo de Assist Chips

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 chip
  • label: Texto en la chip
  • leadingIcon: Icono al inicio de la chip
  • trailingIcon: 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 ejecutada
  • actions: Lista de lectura para dibujar las chips en pantalla y redirigir los clicks a onActionClick()
  • state: Es el estado general de UI, actualizado por map() cuando la acción ejecutada es modificada
  • onActionClick(): 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:

Assist Chips en Android
Assist Chips en Android

Filter Chip

Ejemplo de Filter Chips
Ejemplo de Filter Chips

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 filtro
  • state: Es el resultado de mapear el cambio de los filtros seleccionados y consultar a MemoryShoes con el valor actual
  • mapSizesToFilterState(): Función extraída para convertir la lista de tallas en elementos FilterChipState
  • updateSelectedSizes(): Función extraída para actualizar los filtros seleccionados. Se invoca en onClick 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:

Filter Chips en Android
Filter Chips en Android

Input Chip

Ejemplo de Input Chips
Ejemplo de Input Chips

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 entrada
  • confirmedInputs: Las entradas convertidas en chips, clase ConfirmedInput.
    • label: Etiqueta
    • isSelected: Estado de selección
    • onClick: Acción de click. No usada en este ejemplo
    • onRemove: Acción de click en icono de eliminación
  • onInputChange: La acción cuando se cambia el texto
  • onConfirmInput: La acción cuando se confirma el texto
  • onBackspaceClick: 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 eventos
  • onInputChange(): Actualizamos el valor de la entrada
  • onConfirmInput(): Creamos una nueva chip (buildChip()) y la añadimos al estado
  • onBackspaceClick(): 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:

  1. Usaremos el modificador onGloballyPositioned() para scrollear horizontalmente la fila cuando el ancho cambie
  2. Hacer clic en la fila solicitará el foco para el campo de texto y abrirá el teclado virtual
  3. Usaremos la función InputChipsGroup() para mostrar las chips
  4. 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 texto
  • SpecificTextField(): Es la invocación directa de un OutlinedTextField(). En su interior delegamos las propiedades del estado de UI
  • inputWidth(): Produce el ancho actual del campo de texto basado en la entrada actual. Esto se logra con la utilidad ParagraphIntrinsics(), 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:

Input Chips en acción
Input Chips en acción

Suggestion Chip

Ejemplo de Suggestion Chips
Ejemplo de Suggestion Chips

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:

Suggestion Chips en Acción
Suggestion Chips en Acción

Únete Al Discord De Develou

Si tienes problemas con el código de este tutorial, preguntas, recomendaciones o solo deseas discutir sobre desarrollo Android conmigo y otros desarrolladores, únete a la comunidad de Discord de Develou y siéntete libre de participar como gustes. ¡Te espero!