Home Introduction to Flutter UI Handle User Input

👆 Handle User Input — Let Users Type Their Guesses!

⏱️ 30-35 minutes 📊 Beginner — Make your app interactive! 📦 2 Projects included 🏷️ TextField, Controller, Focus, Callbacks, Buttons

🧒 How Do Apps Listen to Users? (The Mailbox Analogy!)

📬

Imagine Your App Has a Mailbox...

Right now, your Birdle app is like a beautiful painting — it looks nice, but it doesn't do anything when you touch it. To make it interactive, your app needs a way to receive messages from the user.

In Flutter, we use three special tools to listen to what the user types:

⌨️

1. TextField

The mailbox slot — where users type their guesses. It's a box on screen that accepts keyboard input.

📝

2. TextEditingController

The mail clerk — reads what's in the mailbox, clears it out, and delivers the message where it needs to go.

🎯

3. FocusNode

The spotlight — makes sure the cursor stays in the text field so the user can keep typing without clicking again.

By the end of this lesson, players will be able to type their 5-letter guesses into your Birdle game and submit them with a button or the Enter key!

📞

Callbacks — "Call Me When Something Happens!"

A callback is just a fancy word for: "Here's my phone number — call me when you're ready."

When you give a widget a callback function, you're saying: "I don't know exactly when the user will type something, but when they do, run this function."

🔍 A Simple Callback Example

How callbacks work
// Step 1: Define what should happen when text is submitted
void handleGuess(String userTypedThis) {
    print('The user guessed: $userTypedThis');
}

// Step 2: Give this function to the text field
TextField(
    onSubmitted: handleGuess,  // ← "Call handleGuess when user presses Enter"
)

In plain English: onSubmitted is like saying: "Hey TextField, when the user finishes typing and presses Enter, take whatever they typed and pass it to my handleGuess function. I'll handle it from there!"

🖼️

The Complete Flow — From Typing to Game Board

Here's the full journey of a user's guess, from keyboard to colored tiles:

🔄
  1. User types "BIRDS" into the TextField
  2. User presses Enter (or clicks the submit button)
  3. onSubmitted callback fires — it grabs the text from the controller
  4. Controller clears the text field (ready for next guess)
  5. FocusNode keeps the cursor in the text field
  6. onSubmitGuess callback sends the guess to the game logic
  7. Game evaluates the guess and updates the tiles
  8. UI rebuilds — the tiles change colors!

Today you'll build steps 1-6. In the next lesson, you'll learn how the game actually evaluates guesses and updates the UI (steps 7-8).

1 Create the GuessInput Widget Skeleton

First, we'll create a brand new widget called GuessInput that will handle all the typing and submitting logic.

Step 1.1 The Basic Structure with a Callback

Add this code below your Tile class in main.dart:

🔍 The GuessInput Class

lib/main.dart — Add below Tile class
class GuessInput extends StatelessWidget {
    GuessInput({super.key, required this.onSubmitGuess});

    final void Function(String) onSubmitGuess;

    @override
    Widget build(BuildContext context) {
        return Container(); // Placeholder — we'll fill this in!
    }
}

What each line means:

  • class GuessInput extends StatelessWidget — "I'm creating a new widget for typing guesses."
  • required this.onSubmitGuess — "Whoever creates me MUST give me a function to call when the user submits a guess. The required keyword means it's not optional!"
  • final void Function(String) onSubmitGuess — "I'll store that function. It takes a String (the guess) and returns nothing (void)."
  • The build method currently returns an empty Container — we're about to replace it with a real UI!
🧠
Why use a callback? The GuessInput widget doesn't know (or care) what happens to the guess after it's submitted. It just says: "Here's what the user typed — you handle it!" This keeps the input widget reusable — you could use the same GuessInput in a different game without changing it.

2 The TextField — Where Users Type

Now let's replace that empty Container with a real text input! We'll use a Row with an Expanded TextField.

Step 2.1 Build the TextField with Rounded Border

Replace the return Container(); in your GuessInput.build method with:

lib/main.dart — Updated GuessInput.build
@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)),
                            ),
                        ),
                    ),
                ),
            ),
        ],
    );
}

What's new here:

  • Row — We'll add a button next to the text field later, so we need a horizontal layout.
  • Expanded — Tells the TextField: "Take up all the leftover space in the Row!" Without this, the TextField would be tiny.
  • Padding(EdgeInsets.all(8.0)) — Adds 8 pixels of breathing room around the text field.
  • TextField(maxLength: 5) — The star of the show! This creates a text input box. The maxLength: 5 limits guesses to 5 letters (remember, Birdle uses 5-letter words).
  • decoration: InputDecoration(border: OutlineInputBorder(...)) — Gives the text field a nice rounded border (radius 35 makes it pill-shaped).

Step 2.2 Test It! Add GuessInput to Your App

To see the text field, you need to actually use the GuessInput widget in your app. For now, let's add it below your game grid.

Update your MainApp (or wherever your game layout is) to include the GuessInput at the bottom of the Column:

Add GuessInput below your grid
// At the bottom of your Column's children list, add:
GuessInput(
    onSubmitGuess: (guess) {
        print('User guessed: $guess');  // Temporary — just to test
    },
),

Hot reload! You should now see a rounded text input box below your game grid. Try typing in it — you can type up to 5 letters!

3 TextEditingController — Read and Clear Text

A TextEditingController is your remote control for the text field. It lets you read what the user typed, clear the field, and even set text programmatically.

Step 3.1 Add a Controller to Your TextField

Add the controller to your GuessInput class:

lib/main.dart — Add controller
class GuessInput extends StatelessWidget {
    GuessInput({super.key, required this.onSubmitGuess});

    final void Function(String) onSubmitGuess;

    final TextEditingController _textEditingController = TextEditingController();  // ← NEW!

    @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,  // ← Connect controller to TextField
                        ),
                    ),
                ),
            ],
        );
    }
}

How the controller works:

  • _textEditingController.text — Reads whatever is currently typed in the field.
  • _textEditingController.clear() — Erases everything in the field (like pressing a reset button).
  • The underscore _ before the name makes it private — only the GuessInput widget can use it.

Step 3.2 Handle the onSubmitted Event

Now add the onSubmitted callback to capture when the user presses Enter:

Add onSubmitted to TextField
TextField(
    maxLength: 5,
    decoration: const InputDecoration(/* ... */),
    controller: _textEditingController,
    onSubmitted: (_) {                                        // ← Fires when Enter is pressed
        print(_textEditingController.text);             // Print what user typed
        _textEditingController.clear();                        // Clear the field for next guess
    },
),

What's happening:

  • onSubmitted: (_) => { ... } — The underscore _ means "I know there's a parameter (the submitted text), but I don't need to use it directly because I'll get it from the controller instead."
  • print(_textEditingController.text) — Prints the guess to the console so you can verify it's working.
  • _textEditingController.clear() — Empties the text field so the user can type their next guess immediately.

Hot reload, type a 5-letter word in the text field, and press Enter. Check your terminal — you should see the word printed there, and the text field should clear!

4 Focus Control — Keep the Cursor Where It Belongs

Right now, after the user presses Enter, the text field clears but the cursor disappears. The user has to click back into the field to type again. Let's fix that with FocusNode!

Step 4.1 Auto-Focus on Launch

First, make the text field automatically focused when the app starts. Add autofocus: true to your TextField:

Add autofocus
TextField(
    maxLength: 5,
    decoration: const InputDecoration(/* ... */),
    controller: _textEditingController,
    autofocus: true,  // ← Focus this field immediately!
    onSubmitted: (_) {
        print(_textEditingController.text);
        _textEditingController.clear();
    },
),

Now when the app launches, the cursor is already in the text field — the user can start typing immediately!

Step 4.2 Keep Focus After Submission

For the second issue (losing focus after Enter), we need a FocusNode:

Add FocusNode — Complete GuessInput so far
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 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,  // ← Attach the FocusNode
                            onSubmitted: (_) {
                                print(_textEditingController.text);
                                _textEditingController.clear();
                                _focusNode.requestFocus();  // ← Re-focus after clearing!
                            },
                        ),
                    ),
                ),
            ],
        );
    }
}

What FocusNode does:

  • FocusNode() — Creates a "focus manager" that can control where the keyboard cursor is.
  • focusNode: _focusNode — Attaches the focus manager to this specific TextField.
  • _focusNode.requestFocus() — Says: "Hey! Put the cursor back in this text field right now!" This runs after the field is cleared, so the user can immediately type their next guess.

Hot reload! Now when you press Enter, the field clears and the cursor stays right where it is — ready for the next guess!

5 Wire It All Together — Use the onSubmitGuess Callback

Now let's replace the temporary print with the actual callback that was passed into the widget.

Step 5.1 Call onSubmitGuess Instead of print

Update the onSubmitted to use the callback:

Replace print with onSubmitGuess
onSubmitted: (_) {
    onSubmitGuess(_textEditingController.text.trim());  // ← Call the callback!
    _textEditingController.clear();
    _focusNode.requestFocus();
},

What changed:

  • onSubmitGuess(...) — Instead of just printing, we now call the function that was passed into the widget.
  • .trim() — Removes any extra spaces from the beginning or end of the text. So " birds " becomes "birds".
💡
Why .trim()? If a user accidentally types a space before or after their guess (like " bird " instead of "bird"), the game might reject it. trim() cleans up the input automatically — it's a small detail that makes your app feel polished!

6 Add a Submit Button — IconButton

On mobile phones, users expect a button to submit their input — not everyone knows to press Enter! Let's add an icon button next to the text field.

Step 6.1 Add an IconButton to the Row

Add an IconButton as the second child of the Row (after the Expanded TextField):

Complete GuessInput with IconButton
@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: (_) {
                            onSubmitGuess(_textEditingController.text.trim());
                            _textEditingController.clear();
                            _focusNode.requestFocus();
                        },
                    ),
                ),
            ),
            IconButton(                                      // ← NEW: Submit button!
                padding: EdgeInsets.zero,
                icon: const Icon(Icons.arrow_circle_up),
                onPressed: () {
                    onSubmitGuess(_textEditingController.text.trim());
                    _textEditingController.clear();
                    _focusNode.requestFocus();
                },
            ),
        ],
    );
}

What's happening:

  • IconButton — A button that shows an icon instead of text.
  • padding: EdgeInsets.zero — Removes the default padding so the button is compact.
  • Icon(Icons.arrow_circle_up) — Shows an upward arrow in a circle (⤴️) — a common "submit" symbol.
  • onPressed: () { ... } — Does exactly the same thing as pressing Enter: submits the guess, clears the field, and re-focuses.

Hot reload! You should now see an arrow button next to the text field. Click it — it submits the guess just like pressing Enter!

📝 What You Learned Today

You just made your app interactive! Users can now type and submit guesses.

Built a TextField Input

Created a GuessInput widget with a rounded TextField limited to 5 characters using maxLength.

Used TextEditingController

Read user input with .text, cleared the field with .clear(), and cleaned input with .trim().

Controlled Focus

Used autofocus: true for launch focus and FocusNode.requestFocus() to keep focus after submission.

Added Callbacks & Buttons

Implemented onSubmitted and onPressed callbacks, and added an IconButton for mobile-friendly submission.

🧠 Test Yourself!

Let's check your understanding of user input:

Q1 How do you programmatically read or clear the text in a TextField?

Q2 How do you programmatically move focus to a specific TextField?

📦 Project 1: Create a Styled Login Form

Apply your TextField skills to build a different kind of input — a login form!

Project 1 Beginner

Objective: Build a Username & Password Form

📋 Requirements:

  1. Create a new widget called LoginForm (StatelessWidget)
  2. It should have a Column containing:
    • A TextField for "Username" with an appropriate decoration label
    • A SizedBox(height: 16) for spacing
    • A TextField for "Password" with obscureText: true (shows dots instead of letters)
    • A SizedBox(height: 16)
    • An ElevatedButton that says "Login" — when pressed, print both values to the console
  3. Use two separate TextEditingControllers — one for username, one for password
  4. Wrap the Column in a Padding of 16 pixels

🎯 Expected Output:

A centered login form with two text fields (username and password) and a Login button. Pressing the button prints both values.

💡
Hints:
  • Use TextEditingController for each field
  • Use obscureText: true on the password field
  • Use InputDecoration(labelText: 'Username') for labels
  • ElevatedButton(onPressed: () {...}, child: Text('Login'))

📦 Project 2: Build a Search Bar with Clear Button

Create a search bar that has a clear (X) button to reset the input — a common UI pattern!

Project 2 Beginner

Objective: Search Bar with Prefix Icon and Clear Button

📋 Requirements:

  1. Create a new widget called SearchBar
  2. It should have a Row with two children:
    • An Expanded TextField with:
      • A search icon as a prefixIcon in the decoration
      • A hintText saying "Search birds..."
      • A TextEditingController
    • An IconButton with Icons.clear that:
      • Clears the controller text
      • Requests focus back to the TextField
  3. Use a FocusNode to manage focus
  4. Add a callback called onSearch (similar to onSubmitGuess pattern)

🎯 Expected Output:

A search bar with a magnifying glass icon inside the field, hint text, and an X button to clear. Pressing Enter or the X button triggers the onSearch callback.

💡
Hints:
  • Use prefixIcon: Icon(Icons.search) inside InputDecoration
  • Use hintText: 'Search birds...' inside InputDecoration
  • For the clear button: onPressed: () { _controller.clear(); _focusNode.requestFocus(); }
  • Use the same callback pattern as GuessInput: required this.onSearch

🚀 What's Next?

Your app now accepts user input! In the next lesson, you'll learn:

  • StatefulWidgets — How to make your app remember things and update the UI when data changes
  • How to actually evaluate guesses against the hidden word
  • How to update the tile colors based on game logic
🎉

Lesson Complete!

You built a fully interactive text input system! Users can now type guesses, press Enter or click a button to submit, and the field stays focused for the next guess. Your app is becoming a real game!

Click the button above to track your progress!