Saltar al contenido principal

Entrada del usuario

Acepta entrada del usuario con botones y campos de texto.

Aprende a construir entradas de texto, gestionar texto con controladores y manejar las acciones del usuario con botones.

Qué lograrás

Construye un widget de entrada de texto con TextField
Gestiona el texto con TextEditingController
Controla el foco de entrada para una mejor experiencia de usuario
Maneja las acciones del usuario con callbacks y botones

Pasos

1

Introducción

La aplicación mostrará los intentos del usuario en los widgets Tile, pero necesita una forma para que el usuario ingrese esos intentos. En esta lección, construirás esa funcionalidad con dos widgets de interacción: TextField y IconButton.

2

Implementa funciones callback

Para permitir que los usuarios escriban sus intentos, crearás un widget dedicado llamado GuessInput. Primero, crea la estructura básica para tu widget GuessInput que requiera una función callback como argumento. Nombra la función callback onSubmitGuess.

Agrega el siguiente código a tu archivo main.dart.

dart
class GuessInput extends StatelessWidget {
  GuessInput({super.key, required this.onSubmitGuess});

  final void Function(String) onSubmitGuess;

  @override
  Widget build(BuildContext context) {
    // You'll build the UI in the next steps.
    return Container(); // Placeholder
  }
}

La línea final void Function(String) onSubmitGuess; declares a final miembro de la clase llamado onSubmitGuess que tiene el tipo void Function(String). Esta función toma un único argumento String (el intento del usuario) y no devuelve ningún valor (indicado por void).

Esta callback nos indica que la lógica que realmente maneja el intento del usuario se escribirá en otra parte. Es una buena práctica que los widgets interactivos usen funciones callback para mantener el widget que maneja las interacciones reutilizable y desacoplado de cualquier funcionalidad específica.

Para el final de esta lección, la función onSubmitGuess recibida se llamará cuando un usuario ingrese un intento. Primero, necesitarás construir las partes visuales de este widget. Así es como se verá el widget.

Una captura de pantalla de la herramienta property editor de Flutter.
3

El widget TextField

Dado que el campo de texto y el botón se muestran uno al lado del otro, créalos como un widget Row. Reemplaza el marcador de posición Container en tu método build con un Row que contenga un Expanded TextField:

dart
class GuessInput extends StatelessWidget {
  GuessInput({super.key, required this.onSubmitGuess});

  final void Function(String) onSubmitGuess;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextField(
              maxLength: 5,
              decoration: InputDecoration(
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.all(Radius.circular(35)),
                ),
              ),
            ),
          ),
        ),
      ],
    );
  }
}

Ya has visto algunos de estos widgets en lecciones anteriores: Row y Padding. Sin embargo, el widget Expanded es nuevo. Cuando un hijo de un Row (o Column) se envuelve en Expanded, se le indica a ese hijo que llene todo el espacio disponible a lo largo del eje principal (horizontal para Row, vertical para Column) que no haya sido ocupado por otros hijos. Esto hace que el TextField se estire para ocupar todo el espacio excepto el que toman otros widgets en la fila.

El widget TextField también es nuevo en esta lección y es la estrella del espectáculo. Este es el widget básico de Flutter para la entrada de texto.

Hasta ahora, TextField tiene la siguiente configuración.

  • Está decorado con un borde redondeado. Ten en cuenta que la configuración de la decoración es muy similar a cómo se decoran un Container y las cajas.
  • Su propiedad maxLength está configurada en 5 porque el juego solo permite intentos de palabras de 5 letras.
4

Manejar texto con TextEditingController

A continuación, necesitas una forma de gestionar el texto que el usuario escribe en el campo de entrada. Para esto, usa un TextEditingController.

dart
class GuessInput extends StatelessWidget {
  GuessInput({super.key, required this.onSubmitGuess});

  final void Function(String) onSubmitGuess;

  // NEW
  final TextEditingController _textEditingController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextField(
              maxLength: 5,
              decoration: InputDecoration(
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.all(Radius.circular(35)),
                ),
              ),
            ),
          ),
        ),
        //
      ],
    );
  }
}

Se utiliza un TextEditingController para leer, limpiar y modificar el texto en un TextField. Para usarlo, pásalo al TextField.

dart
class GuessInput extends StatelessWidget {
  GuessInput({super.key, required this.onSubmitGuess});

  final void Function(String) onSubmitGuess;

  final TextEditingController _textEditingController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextField(
              maxLength: 5,
              decoration: const InputDecoration(
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.all(Radius.circular(35)),
                ),
              ),
              controller: _textEditingController, // NEW
            ),
          ),
        ),
      ],
    );
  }
}

Ahora, cuando un usuario ingresa texto, puedes capturarlo con el _textEditingController, pero necesitarás saber cuándo capturarlo. La forma más sencilla de reaccionar a la entrada es usando el argumento TextField.onSubmitted. Este argumento acepta una función callback, y esta callback se activa cada vez que el usuario presiona la tecla "Enter" en el teclado mientras el campo de texto tiene el foco.

Por ahora, asegúrate de que esto funcione agregando la siguiente función callback a TextField.onSubmitted:

dart
class GuessInput extends StatelessWidget {
  GuessInput({super.key, required this.onSubmitGuess});

  final void Function(String) onSubmitGuess;

  final TextEditingController _textEditingController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextField(
              maxLength: 5,
              decoration: const InputDecoration(
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.all(Radius.circular(35)),
                ),
              ),
              controller: _textEditingController,
              onSubmitted: (input) {
                // NEW
                print(_textEditingController.text); // Temporary
              },
            ),
          ),
        ),
      ],
    );
  }
}

En este caso, podrías imprimir directamente el input pasado a la callback onSubmitted, pero una mejor experiencia de usuario limpia el texto después de cada intento: Necesitas un TextEditingController para hacer eso. Actualiza el código de la siguiente manera:

dart
class GuessInput extends StatelessWidget {
  GuessInput({super.key, required this.onSubmitGuess});

  final void Function(String) onSubmitGuess;

  final TextEditingController _textEditingController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextField(
              maxLength: 5,
              decoration: const InputDecoration(
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.all(Radius.circular(35)),
                ),
              ),
              controller: _textEditingController,
              onSubmitted: (_) {
                // UPDATED
                print(_textEditingController.text); // Temporary
                _textEditingController.clear(); // NEW
              },
            ),
          ),
        ),
      ],
    );
  }
}
5

Obtener el foco de entrada

A menudo, quieres que una entrada o widget específico obtenga el foco automáticamente sin que el usuario tenga que realizar ninguna acción. En esta aplicación, por ejemplo, lo único que el usuario puede hacer es ingresar un intento, por lo que el TextField debería enfocarse automáticamente cuando se inicia la aplicación. Y después de que el usuario ingresa un intento, el foco debería permanecer en el TextField para que pueda ingresar su siguiente intento.

Para resolver el primer problema de foco, configura la propiedad autofocus en el TextField.

dart
class GuessInput extends StatelessWidget {
  GuessInput({super.key, required this.onSubmitGuess});

  final void Function(String) onSubmitGuess;

  final TextEditingController _textEditingController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextField(
              maxLength: 5,
              decoration: const InputDecoration(
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.all(Radius.circular(35)),
                ),
              ),
              controller: _textEditingController,
              autofocus: true, // NEW
              onSubmitted: (input) {
                print(input); // Temporary
                _textEditingController.clear();
              },
            ),
          ),
        ),
      ],
    );
  }
}

El segundo problema requiere que uses un FocusNode para gestionar el foco del teclado. Puedes usar FocusNode para solicitar que un TextField obtenga el foco (haciendo que el teclado aparezca en dispositivos móviles), o para saber cuándo un campo tiene el foco.

Primero, crea un FocusNode en la clase GuessInput:

dart
class GuessInput extends StatelessWidget {
  GuessInput({super.key, required this.onSubmitGuess});

  final void Function(String) onSubmitGuess;

  final TextEditingController _textEditingController = TextEditingController();

  final FocusNode _focusNode = FocusNode(); // NEW

  @override
  Widget build(BuildContext context) {
    // ...
    return Container();
  }
}

Luego, usa el FocusNode para solicitar el foco cada vez que se envíe el TextField después de limpiar el controlador:

dart
class GuessInput extends StatelessWidget {
  GuessInput({super.key, required this.onSubmitGuess});

  final void Function(String) onSubmitGuess;

  final TextEditingController _textEditingController = TextEditingController();

  final FocusNode _focusNode = FocusNode();

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextField(
              maxLength: 5,
              decoration: const InputDecoration(
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.all(Radius.circular(35)),
                ),
              ),
              controller: _textEditingController,
              autofocus: true,
              focusNode: _focusNode, // NEW
              onSubmitted: (input) {
                print(input); // Temporary
                _textEditingController.clear();
                _focusNode.requestFocus(); // NEW
              },
            ),
          ),
        ),
      ],
    );
  }
}

Ahora, cuando presiones Enter después de ingresar texto, podrás continuar escribiendo.

6

Usar la entrada

Finalmente, necesitas manejar el texto que ingresa el usuario. Recuerda que el constructor de GuessInput requiere una callback llamada onSubmitGuess. En GuessInput, necesitas usar esa callback. Reemplaza la instrucción print con una llamada a esa función.

dart
class GuessInput extends StatelessWidget {
  GuessInput({super.key, required this.onSubmitGuess});

  final void Function(String) onSubmitGuess;

  final TextEditingController _textEditingController = TextEditingController();

  final FocusNode _focusNode = FocusNode();

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextField(
              maxLength: 5,
              decoration: const InputDecoration(
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.all(Radius.circular(35)),
                ),
              ),
              controller: _textEditingController,
              autofocus: true,
              focusNode: _focusNode,
              onSubmitted: (input) {
                onSubmitGuess(_textEditingController.text.trim());
                _textEditingController.clear();
                _focusNode.requestFocus();
              },
            ),
          ),
        ),
      ],
    );
  }
}

El resto de la funcionalidad se maneja en el widget padre, GamePage. En el método build de esa clase, debajo de los widgets Row en los hijos del widget Column, agrega el widget GuessInput:

dart
class GamePage extends StatelessWidget {
  GamePage({super.key});

  final Game _game = Game();

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Column(
        spacing: 5.0,
        children: [
          for (final guess in _game.guesses)
            Row(
              spacing: 5.0,
              children: [
                for (final letter in guess) Tile(letter.char, letter.type),
              ],
            ),
          GuessInput(
            onSubmitGuess: (guess) {
              // TODO, handle guess
              print(guess); // Temporary
            },
          ),
        ],
      ),
    );
  }
}

Por el momento, esto solo imprime el intento para demostrar que está conectado correctamente. Enviar el intento requiere usar la funcionalidad de un StatefulWidget, lo cual harás en la siguiente lección.

7

Botones

Para mejorar la UX en dispositivos móviles y reflejar prácticas de UI bien conocidas, también debería haber un botón que pueda enviar el intento.

Hay muchos widgets de botones integrados en Flutter, como TextButton, ElevatedButton, y el botón que usarás ahora: IconButton. Todos estos botones (y muchos otros widgets de interacción) requieren dos argumentos (además de sus argumentos opcionales):

  • Una función callback pasada a onPressed.
  • Un widget que compone el contenido del botón (a menudo Text o un Icon).

Agrega un botón de icono a la lista de hijos del widget row en el widget GuessInput, y dale un widget Icon para mostrar. El widget Icon requiere configuración; en este caso, la propiedad padding establece en cero el padding entre el borde del botón y el icono que envuelve. Esto elimina el padding por defecto y hace que el botón sea más pequeño.

dart
class GuessInput extends StatelessWidget {
  GuessInput({super.key, required this.onSubmitGuess});

  final void Function(String) onSubmitGuess;

  final TextEditingController _textEditingController = TextEditingController();
  final FocusNode _focusNode = FocusNode();

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(child: Container()),
        IconButton(
          padding: EdgeInsets.zero,
          icon: const Icon(Icons.arrow_circle_up),
          onPressed: null,
        ),
      ],
    );
  }
}

La callback IconButton.onPressed debería resultarte familiar:

dart
class GuessInput extends StatelessWidget {
  GuessInput({super.key, required this.onSubmitGuess});

  final void Function(String) onSubmitGuess;

  final TextEditingController _textEditingController = TextEditingController();
  final FocusNode _focusNode = FocusNode();

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(child: Container()),
        IconButton(
          padding: EdgeInsets.zero,
          icon: const Icon(Icons.arrow_circle_up),
          onPressed: () {
            onSubmitGuess(_textEditingController.text.trim());
            _textEditingController.clear();
            _focusNode.requestFocus();
          },
        ),
      ],
    );
  }
}

Este método hace lo mismo que la callback onSubmitted en el TextField.

8

Revisión

Qué lograste

Aquí tienes un resumen de lo que construiste y aprendiste en esta lección.
Construiste un widget de entrada de texto con TextField

Creaste un widget GuessInput con un TextField para la entrada de texto. Lo configuraste con un borde redondeado, límite de caracteres y usaste Expanded para hacer que llene el espacio disponible en la fila.

Gestionaste el texto con TextEditingController

TextEditingController te permite leer y modificar el contenido del campo de texto. Lo usaste para capturar la entrada del usuario con .text y limpiar el campo después del envío con .clear().

Controlaste el foco de entrada para una UX pulida

Usaste autofocus para enfocar el campo de texto al iniciarse y FocusNode con requestFocus() para mantener el foco después de cada intento. Estos detalles hacen que tu aplicación se sienta responsiva y bien construida.

Manejaste las acciones del usuario con callbacks y botones

Para responder a la entrada del usuario, especificaste funciones callback como onSubmitted y onPressed. Pasar funciones callback como argumentos de constructor mantiene tus widgets reutilizables y desacoplados de una lógica específica.

9

Ponte a prueba

Cuestionario sobre Entrada del Usuario

1 / 2
How do you programmatically read or clear the text in a TextField?
  1. Llamar al método TextField.getText().

    No exactamente.

    TextField no tiene un método getText; usa TextEditingController en su lugar.

  2. Escuchar la callback onChanged y almacenar el valor en una variable.

    No exactamente.

    Aunque onChanged funciona para leer, limpiar requiere un TextEditingController.

  3. Acceder directamente a la propiedad text de TextField.

    No exactamente.

    TextField no expone una propiedad text; necesitas un controlador.

  4. Usar el TextEditingController adjunto al TextField.

    ¡Así es!

    TextEditingController proporciona la propiedad text para leer el valor y el método clear() para restablecerlo.

How do you programmatically move focus to a specific TextField?
  1. Llamar directamente a TextField.focus().

    No exactamente.

    TextField no tiene un método focus; usas un FocusNode.

  2. Envolver el TextField en un GestureDetector y hacer tap programáticamente.

    No exactamente.

    No es así como se gestiona el foco; FocusNode es el enfoque adecuado.

  3. Establecer la propiedad autofocus en true en tiempo de ejecución.

    No exactamente.

    La propiedad 'autofocus' solo funciona en la construcción inicial, no para mover el foco más tarde.

  4. Usar un FocusNode y llamar a requestFocus() en él.

    ¡Así es!

    Un FocusNode te da control sobre el foco, y llamar a requestFocus() mueve el foco a su widget asociado.