Skip to main content

State Lifecycle and TextEditingController

In professional mobile development, stateful components are not eternal: they are born when a user enters a screen, react dynamically to events while the screen is active, and die when the user navigates away.

Understanding the State Lifecycle is essential for initializing resources correctly and, above all, avoiding memory leaks when handling text inputs with TextEditingController.


1. The State Lifecycle Timeline​

Unlike a StatelessWidget (which only contains the build method), the State<T> class moves through several clearly defined stages throughout its lifetime in the Element Tree:

State Lifecycle and TextEditingController Management

The 5 Essential Stages​

  1. createState(): Automatically called by the framework to pair the immutable widget with its new state instance.
  2. initState() (One-Time Initialization):
    • The first method executed after the State object is created.
    • Runs exactly once in the entire lifecycle of the widget.
    • The mandatory place to instantiate controllers (TextEditingController), subscribe to data streams (Streams), or start timers.
    • Strict rule: Must always begin by calling super.initState().
  3. didChangeDependencies(): Executes immediately after initState() and whenever an inherited context object changes (such as the color scheme or screen orientation).
  4. build() (Active Phase): Returns the widget structure to render. Runs initially and repeats every time setState() is invoked.
  5. dispose() (Terminal Destruction and Cleanup):
    • Executes when the State object is permanently removed from the tree (for example, when the user closes the screen or navigates back with Navigator.pop).
    • The mandatory place where you must release and dispose of all heavy resources opened in initState().
    • Strict rule: Must always terminate by calling super.dispose().

2. What is a TextEditingController?​

The TextField widget allows users to enter text using the device keyboard. To read what the user entered, programmatically modify the text, or clear the field after submitting a form, Flutter uses a TextEditingController.

A controller acts as a two-way bridge:

  • Reading: Query the current text via the controller.text property.
  • Writing: Update the text via controller.text = 'New value' or controller.clear().
  • Reactive Listening: Emit events whenever the user presses a key via controller.addListener(...).

3. The Critical Danger of Memory Leaks​

A TextEditingController communicates with native operating system text input services (Android or iOS) and retains listener pointers in RAM.

The Unbreakable Rule of dispose()

If you create a TextEditingController in your screen and fail to destroy it by calling .dispose() inside your State class's dispose() method, Dart's Garbage Collector cannot free that memory.

If the user opens and closes the screen 20 times in a day, 20 orphaned controllers will pile up in device RAM, causing sluggish performance, excessive battery consumption, and eventually an application crash due to out-of-memory errors.


4. Complete Implementation: Form with Validation and Cleanup​

Below is a complete example demonstrating initialization in initState(), usage inside a form, and mandatory release in dispose():

lib/pages/registration_page.dart
import 'package:flutter/material';

class RegistrationPage extends StatefulWidget {
const RegistrationPage({super.key});


State<RegistrationPage> createState() => _RegistrationPageState();
}

class _RegistrationPageState extends State<RegistrationPage> {
// 1. Controller declarations
late final TextEditingController _nameController;
late final TextEditingController _emailController;

// Local state for confirmation messages
String _resultMessage = '';


void initState() {
super.initState();
// 2. Mandatory initialization in initState (happens only once)
_nameController = TextEditingController();
_emailController = TextEditingController();
}


void dispose() {
// 3. MANDATORY CLEANUP: Prevents memory leaks
_nameController.dispose();
_emailController.dispose();
super.dispose();
}

void _submitForm() {
final name = _nameController.text.trim();
final email = _emailController.text.trim();

// Simple mandatory fields validation
if (name.isEmpty || email.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please complete all form fields'),
backgroundColor: Colors.redAccent,
),
);
return;
}

setState(() {
_resultMessage = 'User registered: $name ($email)';
});

// 4. Clear fields after successful submission
_nameController.clear();
_emailController.clear();
}


Widget build(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'User Registration Form',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
// Name Field
TextField(
controller: _nameController,
decoration: const InputDecoration(
labelText: 'Full Name',
hintText: 'e.g., Jane Doe',
prefixIcon: Icon(Icons.person),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
// Email Field
TextField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'Email Address',
hintText: 'user@icesi.edu.co',
prefixIcon: Icon(Icons.email),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 24),
// Submit Button
FilledButton.icon(
onPressed: _submitForm,
icon: const Icon(Icons.check),
label: const Text('Register User'),
),
if (_resultMessage.isNotEmpty) ...[
const SizedBox(height: 24),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFF0FDF4),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFF86EFAC)),
),
child: Text(
_resultMessage,
style: const TextStyle(color: Color(0xFF15803D), fontWeight: FontWeight.w600),
),
),
],
],
),
);
}
}

Self-Assessment Quiz​

Cargando cuestionario...