Saltar al contenido
K
Jetpack ComposeConcepto25 min Intermedio

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ámetroQué esDirección
valueEl texto que el campo enseña ahora mismoBaja (del dueño al campo)
onValueChangeUna 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.

ejemplo mínimo: un campo para buscar libros (con comentarios)
@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 Quijote

Qué hace cada pieza que no habías visto:

PiezaPara 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)
labelEl rótulo. Es un hueco: un trozo de interfaz que le pasas entre llaves; por eso lleva un Text dentro
placeholderUna 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 = trueUna 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.

con el bug: onValueChange no guarda nada
var titulo by rememberSaveable { mutableStateOf("") }
OutlinedTextField(
    value = titulo,
    onValueChange = { },          // avisa, pero nadie cambia `titulo`
    label = { Text("Título del libro") },
)
corregido: onValueChange guarda el texto que llega
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.

con el bug: remember pierde el texto al girar
var titulo by remember { mutableStateOf("") }          // se pierde al girar
corregido: rememberSaveable lo conserva
var titulo by rememberSaveable { mutableStateOf("") }  // sobrevive al giro

Falla 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:

  1. Un campo de una sola línea con el rótulo «Tu nombre». Lo escrito sobrevive al giro.
  2. Debajo, una línea: «Hola, Ana» (con lo que haya escrito) o, si el campo está vacío, «Escribe tu nombre».
  3. 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
app/src/main/java/com/sebastiannarvaez/marcador/ejercicios/saludo/Saludo.kt
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. 1.Un OutlinedTextField recibe value = titulo (guardado con rememberSaveable) y onValueChange = { }, vacío. El usuario pulsa varias teclas. ¿Qué ve?

  2. 2.El usuario tenía escrito «El» y pulsa la «Q». ¿Qué texto recibe onValueChange?

  3. 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. 4.¿En qué se diferencian TextField y OutlinedTextField?

  5. 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?

Faltan 5 de 5