Understanding State and StatefulWidget in Flutter
In Flutter, State is the information that can change during the lifetime of a widget.
When a UI needs to respond to user interaction, changing data, timers, animations, form input, or other
dynamic events, Flutter commonly uses a StatefulWidget.
A StatefulWidget is implemented using two classes: a class that extends
StatefulWidget and a separate class that extends State. The
State object stores mutable data and calls setState() when that data changes,
allowing Flutter to rebuild the relevant UI.
For professional Flutter learning and training, visit
JustAcademy Flutter Training
and use the
Flutter Course Demo Registration
page for a course demonstration.
1. What is State in Flutter?
State is data that can change while a widget is being displayed. The current value
of that data determines what the user sees on the screen.
Examples of state include:
- Counter value
- Whether a button is enabled or disabled
- Whether a checkbox is selected
- Whether a password is visible
- Current slider value
- Selected tab
- Text entered into a form
- Selected product quantity
- Loading status
- Data received from an API
Simple Example of State
Consider a counter application:
int count = 0;
Initially, the value of count is 0. If the user presses a button and the
value becomes 1, the state has changed.
count = count + 1;
In Flutter, changing the variable alone is not enough to update the screen. The framework needs to be
notified that the state changed. This is commonly done with setState().
2. What is a StatefulWidget?
A StatefulWidget is a Flutter widget whose associated state can change during its
lifetime. It is useful when the appearance or data of a widget needs to change in response to events.
Examples include forms, counters, checkboxes, sliders, text fields, animations, loading indicators,
expandable sections, and interactive buttons.
Flutter keeps the widget configuration separate from its mutable state. The
StatefulWidget itself is immutable, while its associated State object can
contain mutable values.
3. StatefulWidget vs StatelessWidget
Feature |
StatelessWidget |
StatefulWidget |
|---|
State changes |
Does not maintain mutable local state |
Can maintain mutable state |
Classes required |
Usually one widget class |
Two classes: StatefulWidget and State |
State class |
Not required |
Required |
setState() |
Not used for local widget state |
Commonly used to trigger rebuilds |
Dynamic UI |
Can display changing parent-provided data, but does not own mutable local state |
Designed for UI that changes over time |
Examples |
Text, Icon, static layout widgets |
Checkbox, Slider, Form, TextField and custom interactive widgets |
4. Basic Structure of StatefulWidget
A StatefulWidget normally contains two related classes:
- The widget class extends
StatefulWidget.
- The state class extends
State<WidgetName>.
import 'package:flutter/material.dart';
class MyWidget extends StatefulWidget {
const MyWidget({super.key});
@override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
@override
Widget build(BuildContext context) {
return const Text('Hello Flutter');
}
}
Important Parts
StatefulWidget defines the widget configuration.
createState() creates the associated State object.
State<MyWidget> stores mutable state.
build() describes the current UI.
setState() tells Flutter that the state has changed and the UI needs rebuilding.
5. Why Are There Two Classes?
Flutter separates the widget configuration from the mutable state because widgets are temporary
configuration objects, while State objects can persist between rebuilds.
The StatefulWidget describes what the widget is, while the State object
manages information that changes while the widget is active.
StatefulWidget
|
| createState()
v
State Object
|
| build()
v
User Interface
This separation allows Flutter to rebuild widget configurations while preserving the associated
state when the framework determines that the widget occupies the same location in the widget tree.
6. Understanding createState()
The createState() method connects the StatefulWidget with its State object.
class Counter extends StatefulWidget {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
Here, Counter is the StatefulWidget and _CounterState is the class that
stores its mutable state.
7. Creating Your First StatefulWidget
Let's create a simple counter application.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: CounterPage(),
);
}
}
class CounterPage extends StatefulWidget {
const CounterPage({super.key});
@override
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
int count = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('State Example'),
),
body: Center(
child: Text(
'Count: $count',
style: const TextStyle(fontSize: 30),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
count++;
});
},
child: const Icon(Icons.add),
),
);
}
}
How This Example Works
CounterPage extends StatefulWidget.
_CounterPageState stores the count variable.
- The initial value of
count is 0.
- The button is pressed by the user.
setState() changes the value.
- Flutter rebuilds the widget.
- The updated value appears on the screen.
8. Understanding setState()
setState() is one of the most important concepts when learning StatefulWidget.
It tells Flutter that the internal state of the widget has changed and that the UI should be rebuilt.
setState(() {
count++;
});
The state-changing operation should normally be performed inside the callback passed to
setState().
Without setState()
count++;
The variable may change internally, but Flutter is not notified through setState().
Therefore, the expected UI update may not occur.
With setState()
setState(() {
count++;
});
Flutter is notified that the state changed and can schedule the widget to rebuild.
9. State Change Flow
The typical flow of a StatefulWidget is:
User Action
|
v
Event Handler
|
v
Change State
|
v
setState()
|
v
Flutter schedules rebuild
|
v
build() runs again
|
v
Updated UI
Example
ElevatedButton(
onPressed: () {
setState(() {
count++;
});
},
child: const Text('Increase'),
)
10. Example: Boolean State
A boolean value is useful for managing states such as showing or hiding content.
class VisibilityExample extends StatefulWidget {
const VisibilityExample({super.key});
@override
State<VisibilityExample> createState() => _VisibilityExampleState();
}
class _VisibilityExampleState extends State<VisibilityExample> {
bool isVisible = true;
@override
Widget build(BuildContext context) {
return Column(
children: [
if (isVisible)
const Text(
'This text is visible',
style: TextStyle(fontSize: 22),
),
ElevatedButton(
onPressed: () {
setState(() {
isVisible = !isVisible;
});
},
child: const Text('Toggle'),
),
],
);
}
}
The value of isVisible changes between true and false.
Calling setState() makes the UI reflect the new value.
11. Example: Checkbox State
class CheckboxExample extends StatefulWidget {
const CheckboxExample({super.key});
@override
State<CheckboxExample> createState() => _CheckboxExampleState();
}
class _CheckboxExampleState extends State<CheckboxExample> {
bool isChecked = false;
@override
Widget build(BuildContext context) {
return Checkbox(
value: isChecked,
onChanged: (value) {
setState(() {
isChecked = value ?? false;
});
},
);
}
}
Here, isChecked represents the current state of the checkbox.
12. Example: Switch State
class SwitchExample extends StatefulWidget {
const SwitchExample({super.key});
@override
State<SwitchExample> createState() => _SwitchExampleState();
}
class _SwitchExampleState extends State<SwitchExample> {
bool isDarkMode = false;
@override
Widget build(BuildContext context) {
return Switch(
value: isDarkMode,
onChanged: (value) {
setState(() {
isDarkMode = value;
});
},
);
}
}
13. Example: TextField and State
Text input is another common example of changing data. A TextEditingController can be
used when the application needs to read or manipulate the entered text.
class NameExample extends StatefulWidget {
const NameExample({super.key});
@override
State<NameExample> createState() => _NameExampleState();
}
class _NameExampleState extends State<NameExample> {
final TextEditingController controller = TextEditingController();
String name = '';
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
controller: controller,
decoration: const InputDecoration(
labelText: 'Enter your name',
),
),
ElevatedButton(
onPressed: () {
setState(() {
name = controller.text;
});
},
child: const Text('Show Name'),
),
Text('Hello $name'),
],
);
}
}
14. StatefulWidget Lifecycle
A StatefulWidget has an associated State object with a lifecycle. Understanding this lifecycle is
important when working with timers, controllers, animations, subscriptions, and other resources.
Common Lifecycle Methods
Method |
Purpose |
|---|
createState() |
Creates the State object associated with the StatefulWidget. |
initState() |
Runs when the State object is initialized. |
didChangeDependencies() |
Runs when dependencies used by the State change. |
build() |
Builds the widget's current UI. |
didUpdateWidget() |
Called when the parent provides a new widget configuration for the same State object. |
dispose() |
Cleans up resources when the State object is permanently removed. |
15. Understanding initState()
initState() is commonly used for one-time initialization of resources associated with
a State object.
@override
void initState() {
super.initState();
print('Widget initialized');
}
Typical uses include:
- Initializing controllers
- Starting certain subscriptions
- Initializing animation controllers
- Preparing initial state
- Setting up resources needed by the widget
16. Understanding dispose()
dispose() is used to clean up resources when the State object is no longer needed.
@override
void dispose() {
controller.dispose();
super.dispose();
}
Controllers, timers, listeners, and other resources that require cleanup should be properly disposed
of when appropriate.
17. Complete Lifecycle Example
class LifecycleExample extends StatefulWidget {
const LifecycleExample({super.key});
@override
State<LifecycleExample> createState() => _LifecycleExampleState();
}
class _LifecycleExampleState extends State<LifecycleExample> {
@override
void initState() {
super.initState();
print('initState called');
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
print('didChangeDependencies called');
}
@override
Widget build(BuildContext context) {
print('build called');
return const Scaffold(
body: Center(
child: Text('Lifecycle Example'),
),
);
}
@override
void dispose() {
print('dispose called');
super.dispose();
}
}
18. The widget Property
Inside the State class, the widget property provides access to the current
StatefulWidget instance.
class UserCard extends StatefulWidget {
final String name;
const UserCard({
super.key,
required this.name,
});
@override
State<UserCard> createState() => _UserCardState();
}
class _UserCardState extends State<UserCard> {
@override
Widget build(BuildContext context) {
return Text(
'User: ${widget.name}',
);
}
}
In this example, name belongs to the widget configuration, while mutable values that
belong to the State object can be stored inside _UserCardState.
19. Widget Configuration vs State
Widget Configuration |
State |
|---|
Provided by the parent |
Maintained by the State object |
Typically stored in final fields |
Can contain mutable values |
Defines configuration |
Tracks changing information |
Example: title |
Example: counter value |
20. Example: Counter with Increase and Decrease
class CounterApp extends StatefulWidget {
const CounterApp({super.key});
@override
State<CounterApp> createState() => _CounterAppState();
}
class _CounterAppState extends State<CounterApp> {
int count = 0;
void increase() {
setState(() {
count++;
});
}
void decrease() {
setState(() {
count--;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Counter'),
),
body: Center(
child: Text(
'$count',
style: const TextStyle(fontSize: 40),
),
),
bottomNavigationBar: Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: decrease,
child: const Text('Decrease'),
),
const SizedBox(width: 16),
ElevatedButton(
onPressed: increase,
child: const Text('Increase'),
),
],
),
),
);
}
}
21. Example: Favorite Button
A favorite button is a practical example of local state. The application can store whether the item
is currently favorited and update the icon when the user taps it.
class FavoriteButton extends StatefulWidget {
const FavoriteButton({super.key});
@override
State<FavoriteButton> createState() => _FavoriteButtonState();
}
class _FavoriteButtonState extends State<FavoriteButton> {
bool isFavorite = false;
@override
Widget build(BuildContext context) {
return IconButton(
onPressed: () {
setState(() {
isFavorite = !isFavorite;
});
},
icon: Icon(
isFavorite
? Icons.favorite
: Icons.favorite_border,
),
);
}
}
22. Example: Password Visibility
class PasswordField extends StatefulWidget {
const PasswordField({super.key});
@override
State<PasswordField> createState() => _PasswordFieldState();
}
class _PasswordFieldState extends State<PasswordField> {
bool obscurePassword = true;
@override
Widget build(BuildContext context) {
return TextField(
obscureText: obscurePassword,
decoration: InputDecoration(
labelText: 'Password',
suffixIcon: IconButton(
icon: Icon(
obscurePassword
? Icons.visibility
: Icons.visibility_off,
),
onPressed: () {
setState(() {
obscurePassword = !obscurePassword;
});
},
),
),
);
}
}
23. Parent and Child State
In a Flutter application, state can be managed by the widget itself, by a parent widget, or through
a combination of approaches. The appropriate choice depends on who needs access to the changing data.
Example
class ParentWidget extends StatefulWidget {
const ParentWidget({super.key});
@override
State<ParentWidget> createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget> {
bool isActive = false;
void updateActive(bool value) {
setState(() {
isActive = value;
});
}
@override
Widget build(BuildContext context) {
return ChildWidget(
isActive: isActive,
onChanged: updateActive,
);
}
}
class ChildWidget extends StatelessWidget {
final bool isActive;
final ValueChanged<bool> onChanged;
const ChildWidget({
super.key,
required this.isActive,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return Switch(
value: isActive,
onChanged: onChanged,
);
}
}
This pattern demonstrates how a parent can own the state while a child receives the current value
and a callback for requesting changes.
24. Local State and App State
Flutter applications can contain different kinds of state. Small pieces of state that belong to one
widget are often called ephemeral or local state. Larger application-wide data may
need broader state-management solutions.
Examples of Local State
- Selected checkbox value
- Current animation state
- Password visibility
- Temporary tab selection
- Current value of a local slider
Examples of App-Level State
- Logged-in user information
- Shopping cart data
- Application settings
- Authentication state
- Data shared by multiple screens
State and setState() are useful for widget-specific state. As an
application becomes more complex, Flutter also provides other state-management approaches.
25. StatefulWidget and User Interaction
StatefulWidget is especially useful when the UI needs to respond to user actions.
User taps button
↓
onPressed()
↓
State changes
↓
setState()
↓
build()
↓
Updated UI
Common interactive widgets include:
Checkbox
Radio
Slider
TextField
Form
Switch
- Custom buttons and interactive components
26. Common Mistakes with StatefulWidget
Mistake 1: Forgetting setState()
count++;
If the change should cause this widget's UI to rebuild, use setState().
setState(() {
count++;
});
Mistake 2: Putting Mutable State in the Widget Class
The StatefulWidget class represents immutable configuration. Mutable state should normally be kept
in its associated State class.
Mistake 3: Forgetting to Dispose Resources
Controllers and other resources that require cleanup should be disposed of appropriately.
@override
void dispose() {
controller.dispose();
super.dispose();
}
Mistake 4: Calling setState() After dispose()
Avoid updating a State object after it has been removed from the widget tree. This is particularly
important when working with asynchronous operations.
Mistake 5: Putting Heavy Work Inside build()
The build() method can run multiple times. Avoid unnecessary expensive operations inside
it. Prepare data appropriately and keep the build method focused on describing the UI.
27. StatefulWidget and Asynchronous Operations
When asynchronous work updates state, you should make sure the State object is still mounted before
calling setState().
Future<void> loadData() async {
final result = await fetchData();
if (!mounted) {
return;
}
setState(() {
// Update state with result
});
}
This helps prevent attempting to update a State object that has already been removed from the widget
tree.
28. Practical Example: Loading State
class LoadingExample extends StatefulWidget {
const LoadingExample({super.key});
@override
State<LoadingExample> createState() => _LoadingExampleState();
}
class _LoadingExampleState extends State<LoadingExample> {
bool isLoading = false;
Future<void> loadData() async {
setState(() {
isLoading = true;
});
await Future.delayed(
const Duration(seconds: 2),
);
if (!mounted) {
return;
}
setState(() {
isLoading = false;
});
}
@override
Widget build(BuildContext context) {
return Center(
child: isLoading
? const CircularProgressIndicator()
: ElevatedButton(
onPressed: loadData,
child: const Text('Load Data'),
),
);
}
}
The isLoading state controls whether the progress indicator or button is displayed.
29. StatefulWidget with a List
StatefulWidget can also be used to update a list dynamically.
class TodoList extends StatefulWidget {
const TodoList({super.key});
@override
State<TodoList> createState() => _TodoListState();
}
class _TodoListState extends State<TodoList> {
final List<String> tasks = [];
void addTask() {
setState(() {
tasks.add('New Task');
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
ElevatedButton(
onPressed: addTask,
child: const Text('Add Task'),
),
Expanded(
child: ListView.builder(
itemCount: tasks.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(tasks[index]),
);
},
),
),
],
);
}
}
30. StatefulWidget and Widget Tree
Flutter applications are represented as widget trees. A StatefulWidget occupies a location in this
tree, while its associated State object can persist across rebuilds when Flutter considers the widget
to represent the same location and identity.
MaterialApp
|
└── Scaffold
|
├── AppBar
|
└── CounterPage
|
├── Text
|
└── Button
When the counter state changes, Flutter rebuilds the relevant widget portion so the updated value can
be displayed.
31. When Should You Use StatefulWidget?
Use a StatefulWidget when a widget needs to maintain changing information during its lifetime.
- When a counter changes.
- When a checkbox changes between checked and unchecked.
- When a switch changes state.
- When a form needs to track input.
- When a widget responds to user interaction.
- When a loading state changes.
- When an animation requires state.
- When local UI data changes over time.
32. When Should You Not Use StatefulWidget?
If a widget only needs to display information received from its parent and does not need to own
mutable local state, a StatelessWidget may be sufficient.
class WelcomeText extends StatelessWidget {
final String name;
const WelcomeText({
super.key,
required this.name,
});
@override
Widget build(BuildContext context) {
return Text('Welcome, $name');
}
}
The widget simply receives name and displays it. It does not need to maintain changing
local state.
33. Best Practices for StatefulWidget
- Keep state as close as practical to the widgets that need it.
- Use
setState() when changing local State that should update the UI.
- Keep the
build() method focused on building the UI.
- Dispose controllers and resources when they are no longer needed.
- Use
initState() for appropriate one-time initialization.
- Be careful with asynchronous operations and disposed State objects.
- Move shared state to an appropriate parent or state-management solution when necessary.
- Use meaningful names for State variables and methods.
- Avoid unnecessary rebuilds and expensive work inside
build().
- Use
const constructors where appropriate to make widget trees clearer and efficient.
34. Interview Questions
Q1. What is State in Flutter?
State is information that can change during the lifetime of a widget and can affect the UI.
Q2. What is a StatefulWidget?
A StatefulWidget is a widget whose associated State object can contain mutable data and trigger UI
updates when that data changes.
Q3. Why does StatefulWidget use two classes?
The StatefulWidget represents immutable configuration, while the State object stores mutable
information and manages the widget's changing UI.
Q4. What does setState() do?
setState() notifies Flutter that the State object has changed and that the widget should
be rebuilt so the UI can reflect the new state.
Q5. What happens if you change State without setState()?
The underlying value may change, but Flutter is not notified through setState(), so the
expected UI rebuild may not happen.
Q6. What is initState()?
initState() is a lifecycle method used for appropriate one-time initialization when a
State object is inserted into the widget tree.
Q7. What is dispose()?
dispose() is used to clean up resources when the State object is permanently removed.
Q8. What is the difference between StatefulWidget and State?
StatefulWidget represents the widget's configuration, while State stores mutable data and provides
the build() implementation for the widget's UI.
Q9. Can a StatefulWidget have constructor parameters?
Yes. A StatefulWidget can receive data from its parent through constructor parameters.
Q10. What is the purpose of createState()?
createState() creates the State object associated with a StatefulWidget.
35. Practice Exercise
Create a Flutter application containing the following features:
- Create a StatefulWidget called
ProfilePage.
- Add a name variable.
- Display the name on the screen.
- Add a button named Change Name.
- When the button is pressed, update the name using
setState().
- Add a boolean variable for online/offline status.
- Add a Switch to change the status.
- Display the current status.
Expected Concept
State
|
├── name
|
└── isOnline
User Interaction
|
v
setState()
|
v
build()
|
v
Updated Profile UI
36. Quick Revision
Concept |
Meaning |
|---|
State |
Changing information that can affect the UI. |
StatefulWidget |
A widget whose associated State can contain mutable data. |
State |
The object that stores mutable state and builds the UI. |
createState() |
Creates the State object. |
setState() |
Notifies Flutter that State changed and the UI should rebuild. |
initState() |
Used for appropriate one-time initialization. |
build() |
Describes the current UI. |
dispose() |
Cleans up resources when the State is removed. |
37. Key Takeaways
- State represents information that can change over time.
- StatefulWidget is useful for dynamic and interactive UI.
- A StatefulWidget normally works together with a separate State class.
- The State object contains mutable data.
createState() connects the widget to its State object.
build() creates the current UI representation.
setState() tells Flutter that local State has changed.
initState() is useful for appropriate initialization tasks.
dispose() is important for cleaning up resources.
- State can be local to one widget or managed at a broader application level.
- Understanding State and StatefulWidget is essential for building interactive Flutter applications.
38. Learning Resources
Conclusion
Understanding State and StatefulWidget is one of the most important
steps in learning Flutter application development. StatelessWidget is useful when a widget does not
need to own mutable local state, while StatefulWidget provides a structure for widgets whose data or
appearance changes during their lifetime.
The core pattern is simple:
store changing data in State → modify it → call setState() → rebuild the UI.
Once this concept is clear, developers can build interactive counters, forms, toggles, lists,
loading screens, animations, and many other dynamic Flutter interfaces.