Campos de texto: TextField
Al terminar: Escribir un campo de texto con su estado en la pantalla y que lo escrito sobreviva a girar el móvil.
Al terminar esta lección sabrás escribir un campo de texto en Compose: un cuadro donde la persona teclea, con su rótulo, y con lo escrito guardado donde toca. Verás por qué el campo no recuerda nada por sí solo, qué pasa si olvidas decirle el texto nuevo (no deja escribir) y qué pasa si lo guardas con remember (al girar el móvil, se borra). No cambias Marcador: practicas en un ejercicio de tu carpeta ejercicios.
Importa porque casi toda app pide texto en algún momento: un nombre, una búsqueda, un dorsal. Y el campo de texto es el mejor ejemplo de lo que acabas de aprender: un composable sin estado, que recibe el texto y avisa de cada tecla. Más adelante, el formulario de «añadir jugador» de Marcador se construye con estas mismas piezas. Temas: TextField, OutlinedTextField, rememberSaveable con texto y dónde vive lo escrito.
El concepto · Un campo que enseña lo que le dices
Un campo de texto (text field) es el cuadro donde el usuario escribe con el teclado. En Compose se llama TextField, y tiene dos parámetros que lo mandan todo:
| Parámetro | Qué es | Dirección |
|---|---|---|
value | El texto que el campo enseña ahora mismo | Baja (del dueño al campo) |
onValueChange | Una función que el campo llama cada vez que el usuario cambia algo. Recibe el texto nuevo entero (no solo la última tecla) | Sube (del campo al dueño) |
Fíjate en lo que no hace el campo: no guarda lo que escribes. Cuando pulsas una tecla, el campo no cambia su propio texto: avisa («el texto debería ser El Q») y espera a que el dueño le devuelva un value nuevo. Si el dueño no se lo devuelve, el campo sigue enseñando lo de antes.
Ejemplo · Buscar un libro
Un ejemplo solo para leer (no lo escribas en Marcador), con datos ajenos a él: el cuadro de búsqueda de una biblioteca. Usa OutlinedTextField, la versión del campo con borde (la diferencia con TextField, más abajo). Los import los propone Android Studio con Option+Enter sobre cada nombre en rojo.
@Composable
fun BuscadorDeLibros() {
// El texto escrito. `rememberSaveable`: sobrevive a recomposiciones y a girar el móvil.
var titulo by rememberSaveable { mutableStateOf("") }
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(
value = titulo, // baja: lo que el campo enseña
onValueChange = { nuevo -> titulo = nuevo }, // sube: cada cambio trae el texto nuevo entero; aquí se guarda
label = { Text("Título del libro") }, // el rótulo: dentro del campo mientras no lo tocas, y sube al borde cuando escribes
placeholder = { Text("Por ejemplo, El Quijote") }, // la pista en gris: solo se ve al tocar el campo, mientras está vacío
singleLine = true, // una sola línea: no crece hacia abajo
)
Text("Buscarás: $titulo") // lee el mismo texto que el campo
}
}Lo que se ve. Un campo con el rótulo «Título del libro» dentro (la pista «Por ejemplo, El Quijote» aparece al tocarlo) y debajo Buscarás: (vacío). Al escribir «El Quijote» letra a letra, onValueChange recibe E, El, El , El Q… y cada vez titulo cambia, la pantalla se recompone y el campo y la línea de debajo enseñan lo mismo:
Buscarás: El QuijoteQué hace cada pieza que no habías visto:
| Pieza | Para qué sirve |
|---|---|
{ nuevo -> titulo = nuevo } | Una lambda: una función sin nombre entre llaves; nuevo -> da nombre a su parámetro, el texto nuevo (son las de Lambdas y funciones de orden superior) |
label | El rótulo. Es un hueco: un trozo de interfaz que le pasas entre llaves; por eso lleva un Text dentro |
placeholder | Una pista en gris que se ve mientras el campo está vacío y se desvanece al escribir. Con label, solo se ve cuando el campo tiene el foco (es el que recibe lo que tecleas) |
singleLine = true | Una sola línea; el campo no crece hacia abajo al llenarse |
TextField o OutlinedTextField
Son el mismo campo con dos aspectos: TextField tiene el fondo relleno (filled) y OutlinedTextField solo un borde (outlined). Los parámetros son los mismos, así que cambiar de uno a otro es cambiar el nombre.
En el formulario de Marcador usarás OutlinedTextField, y es el que lleva el ejercicio de esta lección.
Dos errores típicos
1 · No decirle al campo el texto nuevo. Compila, y el campo no deja escribir: pulsas teclas y no aparece nada.
var titulo by rememberSaveable { mutableStateOf("") }
OutlinedTextField(
value = titulo,
onValueChange = { }, // avisa, pero nadie cambia `titulo`
label = { Text("Título del libro") },
)var titulo by rememberSaveable { mutableStateOf("") }
OutlinedTextField(
value = titulo,
onValueChange = { titulo = it }, // `it`: el nombre que Kotlin da al único parámetro de la lambda (la función entre llaves); aquí, el texto nuevo entero
label = { Text("Título del libro") },
)Falla porque el campo solo enseña value, y value sigue siendo "": la tecla llega, pero nadie la apunta. (En el ejemplo de arriba escribíamos nuevo -> para darle nombre; con it no hace falta, es lo mismo.)
2 · Guardar el texto con remember. Escribes, funciona… y al girar el móvil el campo vuelve a estar vacío.
var titulo by remember { mutableStateOf("") } // se pierde al girarvar titulo by rememberSaveable { mutableStateOf("") } // sobrevive al giroFalla porque al girar, Android destruye la pantalla y crea otra, y remember empieza de cero. Con texto, rememberSaveable funciona directamente: un String cabe en el Bundle.
¿Dónde vive el texto?
Hasta aquí, en la propia pantalla (el composable que contiene el campo). Es lo más sencillo y vale mientras ninguna regla de la app necesite ese texto: el buscador de libros solo lo muestra.
La lógica de negocio son las reglas de la app, las que no dependen de cómo se ve la pantalla. Si una regla necesita el texto («ese nombre ya existe en la plantilla»), el texto sube a un objeto de estado de la pantalla, el ViewModel, que verás más adelante en ViewModel: qué sobrevive, y llega al campo por parámetros. En esta lección no hay ninguna regla, así que el texto se queda en la pantalla.
Reto · Ejercicio: el saludo
En la carpeta ejercicios de tu proyecto (la que creaste en La rotación y la muerte del proceso), crea la subcarpeta saludo: clic derecho sobre com.sebastiannarvaez.marcador → New → Package; el cuadro ya trae com.sebastiannarvaez.marcador escrito, así que añade al final .ejercicios.saludo y pulsa Intro (la carpeta ejercicios ya existe: Android Studio solo crea saludo dentro; aquí «subcarpeta» y «paquete» son lo mismo). Después, clic derecho sobre esa carpeta → New → Kotlin Class/File → File → Saludo. La primera línea debe ser package com.sebastiannarvaez.marcador.ejercicios.saludo.
Escribe un composable Saludo y su vista previa. Lleva:
- Un campo de una sola línea con el rótulo «Tu nombre». Lo escrito sobrevive al giro.
- Debajo, una línea: «Hola, Ana» (con lo que haya escrito) o, si el campo está vacío, «Escribe tu nombre».
- Debajo, un contador con el formato «3/20»: caracteres escritos y máximo (los espacios cuentan). El campo no admite más de 20 caracteres.
Lo que debes ver, en la vista previa estática: el campo vacío, «Escribe tu nombre» y «0/20». Después, ejecuta la vista previa en el emulador (el triángulo verde junto a @Preview, como en Estado en Compose) y escribe «Ana»: «Hola, Ana» y «3/20». Sigue escribiendo hasta llegar a 20: el contador marca «20/20» y los caracteres de más no aparecen. Por último, gira el emulador con los botones de giro de su barra lateral (como en Estado en Compose): lo escrito sigue ahí. Cuando termines, elige otra vez la configuración app en el selector de arriba.
Pistas, todo del ejemplo del libro: el campo y su estado son los mismos; el límite se decide dentro de la función que recibe el texto nuevo, que puede aceptarlo o ignorarlo; la línea de saludo es un if que da un valor (if (condición) uno else otro vale uno o otro, y se puede escribir dentro de Text( … )). Dos piezas de String que necesitarás: texto.length da cuántos caracteres tiene y texto.isEmpty() dice si está vacío (true o false).
Ver solución
package com.sebastiannarvaez.marcador.ejercicios.saludo
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
// `const val`: una constante fija, conocida al compilar. `private`: solo se ve dentro de este fichero. Va fuera de la función, arriba del todo.
private const val MAXIMO = 20 // caracteres que admite el campo
@Composable
fun Saludo(modifier: Modifier = Modifier) {
var nombre by rememberSaveable { mutableStateOf("") } // sobrevive al giro
Column(modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(
value = nombre,
// Solo se acepta el texto nuevo si cabe; si no, `nombre` no cambia y el campo se queda como estaba.
onValueChange = { if (it.length <= MAXIMO) nombre = it },
label = { Text("Tu nombre") },
singleLine = true,
)
Text(if (nombre.isEmpty()) "Escribe tu nombre" else "Hola, $nombre")
Text("${nombre.length}/$MAXIMO")
}
}
@Preview(showBackground = true)
@Composable
private fun SaludoPreview() {
MaterialTheme { Saludo() }
}Si pegas de golpe un texto de 25 caracteres, no entra ninguno: onValueChange recibe el texto nuevo entero, no cabe, y se ignora entero. Recortarlo en vez de ignorarlo es otra decisión posible; aquí se eligió la más corta.
Paso · comprobar que la app no ha cambiado
Objetivo. No hay código nuevo en Marcador: el ejercicio vive en ejercicios y la app no lo usa. Este paso es comprobar que el ejercicio funciona como dice el enunciado y que la app sigue compilando.
Pulsa Run con la configuración app: Marcador se ve y se comporta como al terminar State hoisting. El campo de texto entrará en la app más adelante, en el formulario de añadir un jugador.
Punto de control
- En la vista previa estática de
Saludo, ves el campo «Tu nombre» vacío, «Escribe tu nombre» y «0/20». - Ejecutada en el emulador, escribir «Ana» da «Hola, Ana» y «3/20»; con 20 caracteres el contador marca «20/20» y no entran más.
- Al girar el emulador, lo escrito sigue en el campo.
- Run con la configuración app compila y Marcador se ve igual que antes: la app y sus pruebas no cambian.
Comprueba lo que entendiste
5 preguntas · necesitas 4 para completar la lección
1.Un
OutlinedTextFieldrecibevalue = titulo(guardado conrememberSaveable) yonValueChange = { }, vacío. El usuario pulsa varias teclas. ¿Qué ve?2.El usuario tenía escrito «El» y pulsa la «Q». ¿Qué texto recibe
onValueChange?3.Un campo de texto guarda lo escrito con
var texto by remember { mutableStateOf("") }. El usuario escribe «Ana» y gira el móvil. ¿Qué ve después?4.¿En qué se diferencian
TextFieldyOutlinedTextField?5.Una regla de la app necesita leer el texto de un campo («ese nombre ya existe en la plantilla»). Según la regla de Google, ¿dónde se guarda el texto?