En este tutorial veremos el componente TextField en Compose con el objetivo de crear campos de texto, que le permitan al usuario ingresar y editar texto desde la interfaz gráfica. Exploraremos los dos tipos existentes, sus comportamientos, elementos decorativos, la detección de eventos cuando se cambia el texto, etc.
Lectura: Te recomiendo leer Text En Compose ya que usaremos este componente para crear los campos de texto.
Ejemplo De TextField En Compose
Tomaremos como ilustración pequeños ejemplos asociados a las características en cada sección explicada. Para ello crearemos funciones componibles dentro de varios archivos al interior del paquete examples/TextField
del módulo p7_componentes
.
Y al igual que los demás tutoriales de la guía de Compose, puedes encontrar el proyecto fuente en el repositorio de GitHub.
1. Tipos De Campos De Texto
Filled Text Field
La función componible de nivel superior TextField()
representa al campo de texto lleno, el cual proyecta un contenedor con fondo gris claro con el fin de destacar de otros componentes con que se mezcle.
Si revisas su definición encontrarás la siguiente firma:
@Composable
fun TextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
readOnly: Boolean = false,
textStyle: TextStyle = LocalTextStyle.current,
label: () -> Unit = null,
placeholder: () -> Unit = null,
leadingIcon: () -> Unit = null,
trailingIcon: () -> Unit = null,
isError: Boolean = false,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions(),
singleLine: Boolean = false,
maxLines: Int = Int.MAX_VALUE,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape = MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize),
colors: TextFieldColors = TextFieldDefaults.textFieldColors()
): @Composable Unit
Claramente tenemos muchos atributos por explorar, pero comenzaremos con la forma más básica de TextField
pasando el valor (value
), controlador de cambio de texto (onValueChange
) y etiqueta (label
).
@Composable
fun TextFieldExample() {
var name by remember {
mutableStateOf("Carlos")
}
TextField(
value = name,
onValueChange = { name = it },
label = { Text("Nombre") }
)
}
Debido a que es necesario actualizar el valor del texto del campo de texto en cada evento de tipeo, a value
se le asigna el valor del estado actual y onValueChange
lo actualiza. Lee mi tutorial Estado en Compose para profundizar este tema.
Outlined Text Field
El campo de texto delineado o OutlinedTextField
, proyecta menos énfasis visual que el campo de texto lleno, ya que el fondo de su contenedor es transparente. Dicha naturaleza lo hace excelente candidato para formularios con múltiples campos de texto y pocos componentes de otros tipos.
Su definición en Compose es exactamente igual a la de TextField
, por lo que en el ejemplo anterior puedes reemplazar la función por OutlinedTextField
así:
@Composable
fun OutlinedTextFieldExample() {
var name by remember {
mutableStateOf("Carlos")
}
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Nombre") }
)
}
2. Texto De La Etiqueta
La etiqueta le indica al usuario qué información recibe el campo de texto. Su indicador comienza en la entrada de texto hasta que el campo de texto recibe el foco.
En el caso del filled text field, la etiqueta flota en el medio del contenedor:
Pero para el outlined text field flota hasta su trazo superior:
Define la etiqueta a partir del parámetro label
, el cual recibe un tipo función componible ()->Unit
, en el que podrás especificar el componente Text
con el mensaje de etiqueta.
Adicionalmente, si deseas que aparezca un texto indicativo cuando el campo está vacío y toma el foco, entonces usa el parámetro placeholder
.
El siguiente código produce el campo de texto delineado anterior:
@Composable
fun LabelAndPlaceholderExample() {
var address by remember {
mutableStateOf("")
}
OutlinedTextField(
value = address,
onValueChange = { address = it },
label = { Text("Dirección") },
placeholder = { Text("¿Dónde vives?") }
)
}
3. Elementos De Asistencia
Añadir Texto De Ayuda
A diferencia de su contraparte en el sistema de views (TextInputLayout
), TextField
no posee un parámetro asociado para mostrar un texto de ayuda en la parte inferior del campo de texto.
No obstante, es cuestión de que crees un layout Column
para ubicar un Text
por debajo.
@Composable
fun HelperTextExample() {
var phone by remember {
mutableStateOf("")
}
Column {
TextField(
value = phone,
onValueChange = { phone = it },
label = { Text("Teléfono*") }
)
Text(
text = "*Obligatorio",
color = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.medium),
style = MaterialTheme.typography.caption,
modifier = Modifier.padding(start = 16.dp)
)
}
}
Añadir Mensajes De Error
El mensaje de error reemplaza al texto de ayuda cuando la entrada del usuario en un campo de texto no es válida.
Para determinar si el campo de texto entró a estado de error usa el parámetro booleano isError
. Al pasar true
, Compose usará el color para errores definido en tu tema para colorear al componente.
Por ejemplo: Validar campo requerido al presionar un botón.
@Composable
fun ErrorTextExample() {
var name by remember { mutableStateOf("") }
var nameError by remember { mutableStateOf(false) } // 1
Column {
TextField(
value = name,
onValueChange = {
name = it
nameError = false // 2
},
label = { Text("Nombre") },
isError = nameError // 3
)
val assistiveElementText = if (nameError) "Error: Obligatorio" else "*Obligatorio" // 4
val assistiveElementColor = if (nameError) { // 5
MaterialTheme.colors.error
} else {
MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.medium)
}
Text(// 6
text = assistiveElementText,
color = assistiveElementColor,
style = MaterialTheme.typography.caption,
modifier = Modifier.padding(start = 16.dp)
)
Spacer(Modifier.size(16.dp))
Button(
onClick = { nameError = name.isBlank() }, // 7
modifier = Modifier.align(Alignment.CenterHorizontally)
) {
Text("GUARDAR")
}
}
}
En el código anterior:
- Declaramos un estado para la existencia del error del campo de nombre
- Limpiamos el error cuando se modifique el texto
- Asignamos el estado del error al parámetro
isError
. Esto permitirá actualizar la aparición del mismo en las recomposiciones - Decidimos qué mensaje usaremos, si
nameError
estrue
, mostraremos el mensaje de error, de lo contrario el texto de ayuda - El color también depende de
nameError
al igual que el texto, por lo que usamos los colores del tema error y onSurface - Creamos al elemento de asistencia con los valores anteriores
- Realizamos la validación del nombre con la función de extensión
isBlank()
al interior del botón. Asignamos su resultado anameError
. Al cambiar su estado iniciaremos la recomposición para reflejar el nuevo estado en la interfaz
Mostrar Iconos
Los iconos en un campo de texto tienen diferentes objetivos como: indicar el tipo de contenido, indicar estado de error, proveer acciones de limpieza, disponibilidad de entrada por voz, etc.
Puedes añadir un icono líder al inicio y/o otro al final con los parámetros leadingIcon
y trailingIcon
. Ambos reciben una función componible que representa a su imagen, es decir, a componentes Icon
o IconButton
.
Ejemplo De leadingIcon
Añadir un icono de calendario al inicio de un campo de texto
@Composable
fun LeadingIconExample() {
var createdDate by remember {
mutableStateOf("")
}
TextField(
value = createdDate,
onValueChange = { createdDate = it },
label = { Text("Fecha de inscripción") },
placeholder = { Text("") },
leadingIcon = {
IconButton(onClick = { }) {
Icon(
imageVector = Icons.Filled.DateRange,
contentDescription = "Botón para elegir fecha"
)
}
}
)
}
Ejemplo trailingIcon
El icono de limpieza permite limpiar la entrada de un campo de texto. Este solo aparecerá solo cuando haya texto presente. Veamos cómo ubicar uno:
@Composable
fun TrailingIconExample() {
var name by remember {
mutableStateOf("Carlos")
}
TextField(
value = name,
onValueChange = { name = it },
label = { Text("Nombre") },
trailingIcon = {
if (name.isNotBlank())
IconButton(onClick = { name = "" }) {
Icon(
painter = painterResource(id = R.drawable.ic_cancel),
contentDescription = "Limpiar campo de nombre"
)
}
}
)
}
Como ves, solo si el valor de name
no es blanco, mostramos el IconButton
. En caso de ser mostrado, su controlador onClick
asigna el literal vacío para cadenas a name
.
Contador De Caracteres
El contador de caracteres se usa si deseas establecer un límite de caracteres en la entrada del campo de texto. Este representa la proporción entre los caracteres consumidos y el máximo.
Al igual que los textos de ayuda y error, el contador no está en la preconstrucción de TextField
, por lo que añadimos un componente Text
en la parte inferior derecha.
@Composable
fun CharacterCounterExample() {
var name by remember {
mutableStateOf("")
}
val counterMaxLength = 20 //1
Column {
TextField(
value = name,
onValueChange = {
if (it.length <= counterMaxLength) //2
name = it
},
label = { Text("Nombre") }
)
Text(
text = "${name.length}/$counterMaxLength",//3
color = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.medium),
style = MaterialTheme.typography.caption,
modifier = Modifier
.padding(start = 16.dp)
.align(Alignment.End) //4
)
}
}
En la función componible anterior:
- Declaramos el tamaño máximo de caracteres para el campo de texto del nombre
- Desde
onValueChange
nos preguntamos «¿El tamaño del texto recibido es menor o igual que el límite permitido?». Si es así, entonces actualizamos al estado name - Reflejamos la proporción de la cantidad de caracteres actual con respecto al límite en
text
- Ya que el contador de caracteres va alineado a la derecha del campo de texto, usamos el modificador
align()
para conseguir esta posición
4. Transformaciones Visuales
Usa el parámetro visualTransformation
para asignar una transformación visual al texto de entrada a fin de convertirlo a una presentación para el usuario.
Reemplazar Texto Con Asteriscos En Contraseñas
Compose tiene una transformación visual preconstruida llamada PasswordVisualTransformation
para ocultar el texto de tus campos de texto con contraseñas. Este reemplaza en la escritura cada carácter tipeado por el símbolo *
.
Ejemplo:
Recibir la contraseña del usuario en un campo de texto. Ocultar los caracteres de esta por defecto, pero permitirle revelarlos al presionar un botón de icono.
@Composable
fun PasswordExample() {
var password by remember { mutableStateOf("") }
var hidden by remember { mutableStateOf(true) } //1
OutlinedTextField(
value = password,
onValueChange = { password = it },
label = { Text("Contraseña") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),//2
singleLine = true,
visualTransformation =
if (hidden) PasswordVisualTransformation() else VisualTransformation.None,//3
trailingIcon = {// 4
IconButton(onClick = { hidden = !hidden }) {
val vector = painterResource(//5
if (hidden) R.drawable.ic_visibility
else R.drawable.ic_visibility_off
)
val description = if (hidden) "Ocultar contraseña" else "Revelar contraseña" //6
Icon(painter = vector, contentDescription = description)
}
}
)
}
En el código anterior:
- Declaramos al estado booleano
hidden
para alternar entre los vectores de visibilidad - Usamos el tipo
KeyboardType.Password
para solicitar un teclado virtual para contraseñas - Dependiendo del valor de hidden así mismo se aplica la transformación visual de
PasswordVisualTransformation
oNone
- Añadimos un
trailingIcon
para el campo de texto - Decidimos cuál será el vector según
hidden
- Lo mismo hacemos para el texto de accesibilidad
Texto Como Prefijo/Sufijo
Los prefijos y sufijos de un campo de texto se ubican en el inicio y final del contenedor del campo de texto, con el fin de representar la naturaleza o contexto del contenido ingresado.
Para lograr esta característica, usa una transformación visual personalizada que acomode los textos en el inicio y final del contenedor. Lo que quiere decir que debes crear una clase que implemente a VisualTransformation
.
Ejemplo De Prefijo
Añadir el símbolo del dólar como prefijo a un campo de texto que recibe el precio de un producto
@Composable
fun PrefixExample() {
var price by remember {
mutableStateOf("")
}
TextField(
value = price,
onValueChange = { price = it },
label = { Text("Precio") },
visualTransformation = PrefixVisualTransformation("$ "),
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number)
)
}
La clase PrefixVisualTransformation
concatena el prefijo junto al texto ingresado por el usuario. Adicionalmente, se crea la clase PrefixOffsetMapping
para convertir la posición del cursor del texto original en la del transformado y viceversa:
class PrefixVisualTransformation(private val prefix: String) : VisualTransformation {
override fun filter(text: AnnotatedString): TransformedText {
val transformedText = AnnotatedString(prefix, SpanStyle(Color.Gray)) + text
return TransformedText(transformedText, PrefixOffsetMapping(prefix))
}
}
class PrefixOffsetMapping(private val prefix: String) : OffsetMapping {
override fun originalToTransformed(offset: Int): Int = offset + prefix.length
override fun transformedToOriginal(offset: Int): Int {
val delta = offset - prefix.length
return if (delta < 0) 0 else delta
}
}
Donde:
filter()
: convierte la entrada del usuario en su nueva presentaciónoriginalToTransformed()
: Transforma el offset del texto original al del transformadotransformedToOriginal()
: Traduce el offset del texto transforamdo al original
Ejemplo De Sufijo
Para mostrar un sufijo creamos una transformación visual y mapeador similar a las del prefijo. Lo único es cambiar el orden de la concatenación de los strings y la conversión del desplazamiento del cursor (offset
).
class SuffixVisualTransformation(private val suffix: String) : VisualTransformation {
override fun filter(text: AnnotatedString): TransformedText {
val transformedText = text + AnnotatedString(suffix, SpanStyle(Color.Gray))
return TransformedText(transformedText, SuffixOffsetMapping(text.text))
}
}
class SuffixOffsetMapping(private val originalText: String) : OffsetMapping {
override fun originalToTransformed(offset: Int): Int = offset
override fun transformedToOriginal(offset: Int): Int {
return if (offset > originalText.length) originalText.length else offset
}
}
Por ejemplo:
Creemos un campo de texto que reciba las unidades en kilogramos de un peso y se muestre como sufijo el string " kg"
.
@Composable
fun SuffixExample() {
var weight by remember {
mutableStateOf("")
}
TextField(
value = weight,
onValueChange = { weight = it },
label = { Text("Peso") },
textStyle = TextStyle(textAlign = TextAlign.End),
visualTransformation = SuffixVisualTransformation(" kg"),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
)
}
5. Tipos De Entradas
TextField De Una Sola Línea
Cuando desees desplegar en una sola línea el texto ingresado por el usuario, pasa true
al parámetro singleLine
.
@Composable
fun SingleLineExample() {
var description by remember {
mutableStateOf("Campo de texto")
}
TextField(
value = description,
onValueChange = { description = it },
label = { Text("Descripción") },
singleLine = true,
modifier = Modifier.width(200.dp)
)
}
En el momento que el cursor alcanza el extremo derecho del contenedor, el contenido comenzará a desplazarse hacia la izquierda para dar paso a los nuevos caracteres.
TextField De Múltiples Líneas
Un campo de texto con múltiples líneas comenzará con una sola línea, pero tiene la capacidad de expandirse a fin de mostrar todo el contenido que el usuario ingrese. Esto hace que los elementos que se encuentren por debajo sean desplazados para satisfacer la expansión.
Determina la cantidad de líneas que puede expandirse con el parámetro maxLines
.
@Composable
fun SingleLineExample() {
var description by remember {
mutableStateOf("Campo de texto")
}
TextField(
value = description,
onValueChange = { description = it },
label = { Text("Descripción") },
maxLines = 3,
modifier = Modifier.width(200.dp)
)
}
Si singleLine
está activo, el valor de maxLines
será ignorado.
Ahora bien, maxLines
con el valor de 1 consigue un efecto similar a singleLine
, pero solo singleLine
evita que el teclado ofrezca acción IME de salto de línea.
Áreas De Texto
Son campos de texto con un alto fijo y de mayor tamaño. Cuando se alcanza el límite del contenedor, el cursor hace un salto de línea y el contenido se desplaza hacia abajo.
Para representarlo aplicar algún modificar de altura para el campo de texto en su atributo modifier
.
@Composable
fun TextAreaExample() {
var description by remember {
mutableStateOf("Área de texto con una altura fija de 150dp")
}
TextField(
value = description,
onValueChange = { description = it },
label = { Text("Descripción") },
modifier = Modifier
.width(200.dp)
.height(100.dp)
)
}
TextFields De Solo Lectura
Los campos de texto de solo lectura vienen con un texto precargado que el usuario no puede editar. Sin embargo el usuario podrá copiar su texto o pasarle el foco.
Activa este comportamiento pasando true al parámetro readOnly
de TextField
.
Ejemplo:
@Composable
fun ReadOnlyExample() {
OutlinedTextField(
value = "INV-0001",
onValueChange = { },
label = { Text("Número de factura") },
readOnly = true
)
}
6. Teclado Virtual
Opciones De Teclado
Las opciones de teclado, representadas por la clase KeyboardOptions
, son el mecanismo para configurar el teclado para un TextField
. No obstante, no se asegura que el teclado provea los ajustes solicitados.
Puedes pasar una instancia de estas opciones al parámetro keyboardOptions
de los campos de texto. Su constructor es:
KeyboardOptions(
capitalization: KeyboardCapitalization = KeyboardCapitalization.None,
autoCorrect: Boolean = true,
keyboardType: KeyboardType = KeyboardType.Text,
imeAction: ImeAction = ImeAction.Default
)
Donde:
capitalization
: Indica si el teclado debe capitalizar los caracteres (Characters
), palabras (Words
) o sentencias (Sentences
). Su valor por defecto esNone
para no capitalizar ninguna entradaautoCorrect
: Informa al teclado si debe activar la autocorreciónkeyboardType
: Determina el tipo de teclado usado para el campo de texto. Usa alguno de estos valores:Ascii
,Email
,Number
,NumberPassword
,Phone
,Text
oUri
imeAction
: Especifica el método de entrada (IME) para el teclado. Sus propiedades son:Default
,Done
,Go
,Next
,None
,Previous
,Search
ySend
Por ejemplo:
Mostrar la acción Send
en el teclado virtual para un campo de texto.
@Composable
fun KeyboardOptionsExample() {
var category by remember {
mutableStateOf("")
}
TextField(
value = category,
onValueChange = { category = it },
label = { Text("Nombre de categoría") },
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Send
)
)
}
El código anterior usa el método copy()
para copiar las propiedades de la instancia Default
con el fin de pasar como parámetros sólo los valores que alteramos. En este caso será imeAction
, el cual recibe a ImeAction.Send
.
Procesar Acciones De Teclado
Complementando, si deseas ejecutar acciones cuando el usuario ha disparado la acción del botón IME desde el teclado, entonces usa el parámetro keyboardActions
.
Este recibe una instancia de la clase KeyboardActions
, la cual posee el siguiente constructor:
KeyboardActions(
onDone: KeyboardActionScope.() -> Unit = null,
onGo: KeyboardActionScope.() -> Unit = null,
onNext: KeyboardActionScope.() -> Unit = null,
onPrevious: KeyboardActionScope.() -> Unit = null,
onSearch: KeyboardActionScope.() -> Unit = null,
onSend: KeyboardActionScope.() -> Unit = null
)
Como ves, son tipos función que podrás especificar para ejecutar las sentencias que desees según la acción IME.
Ejemplo:
Procesar la acción IME del ejemplo anterior para guardar el nombre de la categoría y mostrarla en pantalla
@Composable
fun KeyboardActionsExample() {
var category by remember {
mutableStateOf("")
}
var categories by remember {
mutableStateOf("Categorías:\n")
}
Column {
TextField(
value = category,
onValueChange = { category = it },
label = { Text("Nombre de categoría") },
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Send
),
keyboardActions = KeyboardActions(onSend = {
categories += "- $category\n"
})
)
Spacer(Modifier.size(16.dp))
Text(categories)
}
}
En este ejemplo solo usamos el parámetro onSend, pasandole una lambda que actualiza el estado de un Text que muestra la concatenación de las categorías que van siendo guardadas.
En el caso de que quieras procesar cualquier acción en un solo lugar, usa la función KeyboardActions()
, la cual recibe al parámetro onAny
que será aplicada a todas las acciones:
keyboardActions = KeyboardActions(onAny = {
categories += "- $category\n"
})
Ocultar Teclado
La propiedad de composición local que se asocian con el cierre del teclado es la interfaz SoftwareKeyboardController
. Puedes obtener una implementación a través de LocalSoftwareKeyboardController
y usar sus métodos hide()
/show()
para ocultar/mostrar el teclado.
Ejemplo:
Ocultar teclado luego de que se realiza la acción IME del ejemplo anterior.
@ExperimentalComposeUiApi
@Composable
fun KeyboardActionsExample() {
//...
val keyboardController = LocalSoftwareKeyboardController.current
Column {
TextField(
//...
keyboardActions = KeyboardActions(onSend = {
keyboardController?.hide()
//...
})
)
//...
}
}
En primer lugar obtenemos el controlador en la variable keyboardController
y luego cuando se ejecute la acción invocamos su método hide()
.
7. Aplicar Estilos A TextField
Recuerda que el TextField
hace parte de la categoría de componentes pequeños comprendidos en el sistema de Material Design, por lo que el tema que apliques definirá su figura, fuente tipográfica y colores usados.
Pero si deseas cambiar estos aspectos en un solo campo de texto, entonces usa los parámetros:
textStyle
: Define el estilo de un texto con una instancia de la claseTextStyle
colors
: Aplica los colores del campo de texto con una instancia del tipoTextFieldColors
. Invoca a la función componibletextFieldColors()
para definir el color de cada decoración del campo de textoshape
: La forma del campo de texto determinada por un tipoShape
Ejemplo:
Personalizar la familia tipográfica del texto de entrada, los colores y forma de un campo de texto.
@Composable
fun TextFieldStyleExample() {
var nameOnCard by remember {
mutableStateOf("Américo Vespucio")
}
val fontFamily = FontFamily(Font(R.font.playfairdisplay_regular))
val color = Color(0xFF120524)
val shape = CutCornerShape(ZeroCornerSize)
TextField(
value = nameOnCard,
onValueChange = { nameOnCard = it },
label = { Text("Nombre en la tarjeta") },
textStyle = TextStyle(fontFamily = fontFamily),
colors = TextFieldDefaults.textFieldColors(
textColor = color,
backgroundColor = Color.White,
focusedLabelColor = color.copy(alpha = ContentAlpha.high),
focusedIndicatorColor = Color.Transparent,
cursorColor = color,
),
shape = shape,
modifier = Modifier.border(1.dp, color)
)
}
Como ves, fontFamily
aplica la fuente Playfair Display, color
usa un vinotinto oscuro y shape
crea una elemento CutCornerShape
sin afección en sus esquinas.
La función textFieldColors()
permite asignar al parámetro colors
el color para todos los elementos del TextField
. En este caso usamos:
textColor
: El color del texto de entradabackgroundColor
: El color de fondo del contenedor del campo de textofocusedLabelColor
: Color de la etiqueta cuando el campo tiene el focofocusedIndicatorColor
: Color para el indicador que recubre al campo de textocursorColor
: Es el color del cursor que titila al escribir
Ú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!