Flutter’s UI framework is declarative and reactive, meaning the user interface is rebuilt whenever the underlying state changes. To manage this properly, Flutter provides lifecycle methods within State objects of StatefulWidgets.
Among the most important lifecycle methods are:
initState– Called once when the widget is first created.didChangeDependencies– Called when widget dependencies change.dispose– Called when the widget is permanently removed from the widget tree.
Understanding these methods is crucial for efficient resource management, performance optimization, and predictable widget behavior.
This post provides a deep dive into these lifecycle methods, their use cases, best practices, and real-world coding examples.
Introduction to the Widget Lifecycle
Before diving into initState, didChangeDependencies, and dispose, let’s recap how StatefulWidget works.
A StatefulWidget has two classes:
- The widget class – immutable and used to configure the widget.
- The state class – mutable and holds the widget’s data and lifecycle methods.
Example structure:
class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
@override
Widget build(BuildContext context) {
return Container();
}
}
MyWidgetis the widget configuration._MyWidgetStatemanages lifecycle and state changes.
The lifecycle of a State object moves through creation, updates, and destruction.
Overview of Lifecycle Methods in State
The key lifecycle methods in a State object include:
initState– Called only once when the widget is inserted into the widget tree.didChangeDependencies– Called when the widget’s dependencies change (e.g., inherited widgets).build– Called whenever the widget needs to rebuild.dispose– Called when the widget is removed from the tree.
This post focuses on initState, didChangeDependencies, and dispose, as these are most critical for managing resources.
initState in Flutter
What is initState?
The initState method is called once when the State object is first created. It is used to perform initialization tasks such as:
- Setting initial values.
- Starting animations.
- Initializing controllers (e.g.,
TextEditingController,AnimationController). - Fetching data once before the first build.
It is called before build, but you cannot use BuildContext to access inherited widgets directly inside initState because the widget tree is not fully built yet.
Syntax
@override
void initState() {
super.initState();
// Initialization code here
}
Always call super.initState() at the beginning.
Example of initState
class InitStateExample extends StatefulWidget {
@override
_InitStateExampleState createState() => _InitStateExampleState();
}
class _InitStateExampleState extends State<InitStateExample> {
late String message;
@override
void initState() {
super.initState();
message = "Hello, Flutter!"; // Initialize variable
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(child: Text(message)),
);
}
}
In this example, the message variable is initialized once when the widget is created.
Best Practices for initState
- Use it for initialization only, not for UI updates.
- Initialize controllers and other resources here.
- Avoid calling
BuildContextdependent code directly; instead, usedidChangeDependencies. - Always call
super.initState()first.
didChangeDependencies in Flutter
What is didChangeDependencies?
The didChangeDependencies method is called when:
- The state object is first created (after
initState). - The dependencies of the widget change, such as when an inherited widget updates.
This is the right place to use BuildContext to access inherited widgets like Theme, MediaQuery, or Provider.
Syntax
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Access dependencies here
}
Example of didChangeDependencies
class DependenciesExample extends StatefulWidget {
@override
_DependenciesExampleState createState() => _DependenciesExampleState();
}
class _DependenciesExampleState extends State<DependenciesExample> {
late double screenWidth;
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Safe to use BuildContext here
screenWidth = MediaQuery.of(context).size.width;
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(child: Text('Screen width: $screenWidth')),
);
}
}
Here, MediaQuery is accessed inside didChangeDependencies, not initState.
When to Use didChangeDependencies?
- When you need to access inherited widgets like
Theme,MediaQuery, or data provided byProvider. - When the widget depends on external state that can change during its lifecycle.
- For expensive operations that depend on inherited values.
dispose in Flutter
What is dispose?
The dispose method is called when the State object is permanently removed from the widget tree. This is where you should release resources to avoid memory leaks.
Syntax
@override
void dispose() {
// Clean up resources here
super.dispose();
}
Example of dispose
class DisposeExample extends StatefulWidget {
@override
_DisposeExampleState createState() => _DisposeExampleState();
}
class _DisposeExampleState extends State<DisposeExample> {
late TextEditingController controller;
@override
void initState() {
super.initState();
controller = TextEditingController();
}
@override
void dispose() {
controller.dispose(); // Free the resource
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: TextField(controller: controller),
);
}
}
Here, the TextEditingController is disposed of to free memory.
When to Use dispose?
- Dispose controllers like:
TextEditingControllerAnimationControllerScrollController
- Cancel timers or streams.
- Close database connections.
- Release any other resource tied to the widget.
Full Lifecycle Example
Here’s a complete example showing initState, didChangeDependencies, and dispose:
class FullLifecycleExample extends StatefulWidget {
@override
_FullLifecycleExampleState createState() => _FullLifecycleExampleState();
}
class _FullLifecycleExampleState extends State<FullLifecycleExample> {
late TextEditingController controller;
late double screenHeight;
@override
void initState() {
super.initState();
controller = TextEditingController();
print("initState called");
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
screenHeight = MediaQuery.of(context).size.height;
print("didChangeDependencies called");
}
@override
void dispose() {
controller.dispose();
print("dispose called");
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
TextField(controller: controller),
Text('Screen height: $screenHeight'),
],
),
);
}
}
This demonstrates how initialization, dependency changes, and cleanup occur during the widget’s lifecycle.
Common Mistakes and How to Avoid Them
- Using BuildContext in initState:
Avoid using context ininitState. UsedidChangeDependenciesinstead. - Forgetting dispose:
Not disposing controllers and streams leads to memory leaks. - Heavy Work in initState:
Performing expensive work ininitStatecan delay rendering. UseFuture.microtaskorWidgetsBinding.instance.addPostFrameCallbackif needed. - Ignoring didChangeDependencies:
Developers often ignoredidChangeDependencies, but it’s crucial when using inherited widgets.
Best Practices for Lifecycle Methods
- initState: Initialize variables, start animations, set up listeners.
- didChangeDependencies: Access
BuildContextand inherited data safely. - dispose: Clean up resources properly to avoid memory leaks.
- Always call the super method (
super.initState(),super.didChangeDependencies(),super.dispose()).
Real-World Use Cases
- initState
- Start an animation with
AnimationController. - Initialize a database connection.
- Fetch initial data from an API.
- Start an animation with
- didChangeDependencies
- Get screen size from
MediaQuery. - Access theme or localization.
- Use values provided by
ProviderorInheritedWidget.
- Get screen size from
- dispose
- Dispose animation controllers to stop animations.
- Cancel subscriptions to streams.
- Close sockets or connections.
Leave a Reply