
Flutter: Why Does My TextField Lose Focus?
Keyboard closes after every keystroke? You likely initialized the controller inside build(). Master the lifecycle of TextEditingController.

Keyboard closes after every keystroke? You likely initialized the controller inside build(). Master the lifecycle of TextEditingController.
Yellow stripes appear when the keyboard pops up? Learn how to handle layout overflows using resizeToAvoidBottomInset, SingleChildScrollView, and tricks for chat apps.

Push works on Android but silent on iOS? Learn to fix APNs certificates, handle background messages, configure Notification Channels, and debug FCM integration errors.

Class is there, but style is missing? Debugging Tailwind CSS like a detective.

Think Android is easier than iOS? Meet Gradle Hell. Learn to fix minSdkVersion conflicts, Multidex limit errors, Namespace issues in Gradle 8.0, and master dependency analysis with `./gradlew dependencies`.

It's the #1 most baffling bug for Flutter devs. You tap the Search bar, type "F". The keyboard slides down instantly, and the cursor (Focus) disappears.
You tap again, type "l". Gone. "u"... Gone. "t"... Gone. The app is playing "Red Light, Green Light" with me.
This isn't just a bug. It's a warning: "You are defying the Flutter Lifecycle."
build()99% of the time, your code looks like this:
class SearchPage extends StatelessWidget { // 1. Stateless
@override
Widget build(BuildContext context) {
// 2. 😱 Create Controller INSIDE build
final controller = TextEditingController();
return TextField(
controller: controller,
onChanged: (text) {
// 3. State Change -> Trigger Rebuild (e.g., Provider/GetX)
someState.update(text);
},
);
}
}
Let's trace the execution:
onChanged runs, forcing a State update elsewhere.build() method runs AGAIN.final controller = TextEditingController(); runs AGAIN.TextField sees a new controller and swaps it in.TextEditingController IS State.
State must not be reset on every build. It must outlive the build cycles.
So, you MUST use StatefulWidget.
class SearchPage extends StatefulWidget {
@override
_SearchPageState createState() => _SearchPageState();
}
class _SearchPageState extends State<SearchPage> {
// ✅ 1. Declare as Class Member (Stored in State)
late TextEditingController _controller;
@override
void initState() {
super.initState();
// ✅ 2. Initialize ONLY ONCE
_controller = TextEditingController();
}
@override
void dispose() {
// ✅ 3. Cleanup Memory (Required)
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return TextField(
controller: _controller, // This variable stays same across builds
...
);
}
}
Now, even if build() runs a thousand times, _controller remains the exact same instance created in initState(). Focus is preserved.
"Boilerplate is annoying. init? dispose? Too much code."
If you like React Hooks, use them in Flutter with flutter_hooks.
It magically reduces code.
class SearchPage extends HookWidget { // Extend HookWidget
@override
Widget build(BuildContext context) {
// ✅ useTextEditingController: Handles lifecycle automatically
final controller = useTextEditingController();
return TextField(
controller: controller,
...
);
}
}
useTextEditingController stores the state internally and returns the existing instance on rebuilds. Elegant.
Key Issue (Abuse of GlobalKey)Rarely, focus gets lost even with proper controllers.
This happens when the Widget's Key changes.
Flutter identifies widgets in the tree using runtimeType and Key.
If a parent rebuilds and the TextField gets a different Key, Flutter thinks, "Oh, this is a DIFFERENT widget," destroys the old one, and creates a fresh one. Focus is lost.
// ❌ Bad: New Key on every build
TextField(
key: GlobalKey(), // Every build -> New Key -> New Widget!
)
Don't use GlobalKey unless absolutely necessary (e.g., accessing state from outside). If you do use it, declare it as a final variable in your State/Class, never inline in build().
It's not just TextEditingController. You must manage FocusNode too.
Essential for "Next" button functionality.
class _LoginPageState extends State<LoginPage> {
late FocusNode _emailFocus;
late FocusNode _pwFocus;
@override
void initState() {
super.initState();
_emailFocus = FocusNode();
_pwFocus = FocusNode();
}
@override
void dispose() {
// 💀 Memory Leak if you forget this!
_emailFocus.dispose();
_pwFocus.dispose();
super.dispose();
}
void _nextField() {
_emailFocus.unfocus();
FocusScope.of(context).requestFocus(_pwFocus);
}
}
FocusNode IS State. Creating it inside build will reset focus on every keypress.
Login button looks great at the bottom. Tap email -> Keyboard rises -> Button executes a disappearing act (or Bottom Overflow Error).
SingleChildScrollViewMake the screen scrollable.
resizeToAvoidBottomInsetA Scaffold property. Default is true.
Set to false if you want the background image to stay still (behind the keyboard) instead of squishing up.
"Hide the Logo when typing to save space."
// Check keyboard height
final isKeyboardVisible = MediaQuery.of(context).viewInsets.bottom > 0;
return Column(
children: [
if (!isKeyboardVisible) BigLogo(), // Hide logo to save space
TextField(...),
],
);
UX Magic.
Users expect "Tap Background == Done".
GestureDetector(
onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
child: Scaffold(...),
)
Use FocusManager. It's cleaner and safer than calling low-level SystemChannels.
"Focus Lost" = "TextField was Re-created"
TextEditingController is declared.
build()? -> ❌ You are the culprit.State class? -> ✅ Correct.StatefulWidget.
flutter_hooks if you hate boilerplate.Once you respect the Lifecycle, the keyboard will stop playing hide-and-seek with you.
Using TextField inside showModalBottomSheet often results in the keyboard covering the input.
isScrollControlled: true & Padding
showModalBottomSheet(
context: context,
isScrollControlled: true, // 1. Allow full height
builder: (context) => Padding(
// 2. Add padding for keyboard
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom
),
child: MyTextFieldWidget(),
),
);
Without this, the OS keyboard will brutally cover your bottom sheet.
StatefulWidget to flutter_hooksProblem:
Your StatefulWidget code is 50+ lines long with noisy init and dispose logic.
Challenge:
Reduce it to 15 lines using flutter_hooks.
class Search extends StatefulWidget { ... }
class _SearchState extends State<Search> {
late TextEditingController _c;
@override
void initState() { super.initState(); _c = TextEditingController(); }
@override
void dispose() { _c.dispose(); super.dispose(); }
@override
Widget build(BuildContext context) { return TextField(controller: _c); }
}
After:
class Search extends HookWidget {
@override
Widget build(BuildContext context) {
final c = useTextEditingController();
return TextField(controller: c);
}
}
Experience the elegance of Hooks.
Q: autofocus: true doesn't work!
A: You likely requested focus before the page transition animation finished. Use addPostFrameCallback or a small delay.
Q: I want to save data on Blur (Focus Lost).
A: Add a listener to FocusNode.
focusNode.addListener(() {
if (!focusNode.hasFocus) {
saveData();
}
});