flutter-textfield-controller-focus

Campos de texto en Flutter: controller, decoración y foco

  • 4 min

Un input de texto es un widget que permite al usuario introducir y editar texto.

Ya sabemos tocar la pantalla, ahora vamos a aprender a escribir en ella.

El teclado virtual es la principal vía de entrada de datos en un móvil. Ya sea para un login, un buscador o un chat, necesitamos dominar el widget TextField.

A diferencia de un simple botón, un campo de texto tiene mucha complejidad interna: gestión del teclado, posición del cursor, selección de texto, ocultación de contraseñas… Por suerte, Flutter nos lo da (casi) todo hecho.

El widget TextField

En su forma más básica, es un componente que al tocarlo despliega el teclado del sistema.

TextField() // Así de simple, ya funciona
Copied!

Pero claro, esto es solo una línea subrayada. Para que sea usable, necesitamos decorarlo y, lo más importante, leer lo que el usuario escribe.

Capturar el texto con TextEditingController

Hay dos formas de leer el texto: el callback onChanged y el controlador.

  • onChanged: Te avisa cada vez que el usuario pulsa una tecla. Es útil para buscadores en tiempo real.
  • TextEditingController: permite leer el texto cuando queramos (por ejemplo, al pulsar Guardar) y también modificarlo desde código.

Implementar el controlador

El controlador es un objeto que debe vivir durante todo el ciclo de vida del Widget. Por tanto, necesitamos un StatefulWidget.

class FormularioSimple extends StatefulWidget {
  @override
  _FormularioState createState() => _FormularioState();
}

class _FormularioState extends State<FormularioSimple> {
  // 1. Instanciamos el controlador
  final TextEditingController _emailController = TextEditingController();

  @override
  void dispose() {
    // 2. ¡IMPORTANTE! Limpiamos memoria al salir
    _emailController.dispose();
    super.dispose();
  }

  void _enviarDatos() {
    // 3. Accedemos al texto actual con .text
    print("Email introducido: ${_emailController.text}");
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(
          controller: _emailController, // 4. Lo vinculamos al widget
          decoration: InputDecoration(labelText: "Email"),
        ),
        ElevatedButton(onPressed: _enviarDatos, child: Text("Enviar"))
      ],
    );
  }
}
Copied!

Ojo con la memoria: Un TextEditingController se queda escuchando eventos nativos. Si olvidas llamar a dispose() cuando la pantalla se cierra, crearás un Memory Leak (fuga de memoria).

Decoración: InputDecoration

El aspecto predeterminado depende del tema Material de la aplicación. Para añadir bordes, iconos y textos auxiliares, usamos la propiedad decoration con la clase InputDecoration.

Aquí están las propiedades que usarás el 90% de las veces:

TextField(
  decoration: InputDecoration(
    // Etiqueta que flota arriba al hacer foco
    labelText: "Nombre de Usuario",
    
    // Texto de ayuda (placeholder) que desaparece al escribir
    hintText: "Ej: Luis Llamas",
    
    // Icono a la izquierda
    prefixIcon: Icon(Icons.person),
    
    // Icono a la derecha (útil para borrar texto o ver password)
    suffixIcon: Icon(Icons.check_circle),
    
    // Borde completo (cajita)
    border: OutlineInputBorder(
      borderRadius: BorderRadius.circular(10)
    ),
  ),
)
Copied!

Tipo de teclado y acción

No hay nada más frustrante que te pidan el teléfono y salga el teclado de letras completo.

Podemos (y debemos) ayudar al usuario configurando el tipo de teclado (keyboardType) y la acción del botón “Enter” (textInputAction).

TextField(
  // Muestra teclado numérico, de email, de url, etc.
  keyboardType: TextInputType.emailAddress, 
  
  // Cambia el botón "Enter" por "Siguiente", "Buscar", "Enviar"...
  textInputAction: TextInputAction.next,
  
  // Oculta el texto (puntitos) para contraseñas
  obscureText: true, 
)
Copied!

Gestión del foco con FocusNode

El “Foco” determina qué campo de texto está activo y recibiendo input del teclado.

A veces queremos controlar esto manualmente. El caso de uso típico es: “Cuando el usuario pulse ‘Siguiente’ en el campo Email, quiero que el foco salte automáticamente al campo Contraseña”.

Para esto usamos los FocusNode.

class FormularioAvanzado extends StatefulWidget {
  @override
  _FormularioAvanzadoState createState() => _FormularioAvanzadoState();
}

class _FormularioAvanzadoState extends State<FormularioAvanzado> {
  // Nodos de foco
  final FocusNode _nombreFocus = FocusNode();
  final FocusNode _passFocus = FocusNode();

  @override
  void dispose() {
    // También hay que limpiarlos
    _nombreFocus.dispose();
    _passFocus.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(
          focusNode: _nombreFocus, // Asignamos nodo 1
          textInputAction: TextInputAction.next, // Botón "Siguiente"
          onSubmitted: (_) {
            // Al pulsar enter/siguiente, pedimos foco para el nodo 2
            FocusScope.of(context).requestFocus(_passFocus);
          },
        ),
        TextField(
          focusNode: _passFocus, // Asignamos nodo 2
        ),
      ],
    );
  }
}
Copied!

El truco para cerrar el teclado

Un comportamiento esperado en móviles es que, si toco en un espacio vacío fuera del teclado, este se cierre. En Flutter esto no pasa por defecto.

Tenemos que implementarlo nosotros envolviendo la pantalla en un GestureDetector:

GestureDetector(
  onTap: () {
    // Esto quita el foco de cualquier input, cerrando el teclado
    FocusScope.of(context).unfocus();
  },
  child: Scaffold( ... ),
)
Copied!