Stateful Widgets in Flutter – Detailed Notes
A StatefulWidget is a Flutter widget whose associated state can change during the lifetime of the widget. Examples include counters, checkboxes, switches, sliders, forms, text fields, loading indicators, and other interactive UI components.
A StatefulWidget itself is immutable. Its mutable data is stored in a separate State object. When that state changes, setState() tells Flutter to rebuild the relevant UI. :contentReference[oaicite:0]{index=0}
Related Flutter Training: JustAcademy Flutter Training | Register for Flutter Course Demo
1. What is a StatefulWidget?
A StatefulWidget is a widget used when part of the user interface needs to change dynamically during the lifetime of the widget.
For example, consider a counter application:
Count: 0
When the user presses a button:
Count: 1
Pressing it again changes the value:
Count: 2
Because the displayed value changes, a StatefulWidget is appropriate for managing this changing state.
2. What is State?
State is information that can change while a widget is being used. Flutter defines state as information that can be read synchronously when the widget is built and may change during the widget's lifetime. :contentReference[oaicite:1]{index=1}
Examples of state include:
- Counter value
- Whether a checkbox is selected
- Whether a switch is enabled
- Selected tab
- Selected dropdown value
- Current slider value
- Loading status
- Form input values
- Whether a password is visible
- Whether a menu is expanded
3. StatefulWidget vs StatelessWidget
| Feature |
StatelessWidget |
StatefulWidget |
| State |
Does not hold mutable local state |
Can manage mutable state |
| State class |
Not required |
Requires a separate State class |
| Dynamic UI |
Usually depends on immutable configuration and context |
Can change in response to state changes |
| setState() |
Not available |
Available in the State class |
| createState() |
Not required |
Required |
| Common examples |
Text, Icon, simple layouts |
Checkbox, Slider, TextField, interactive custom widgets |
4. Why Do We Need Stateful Widgets?
Applications frequently need to respond to user actions or changing data.
For example:
User taps button
↓
State changes
↓
setState()
↓
build() runs again
↓
Updated UI
Without a mechanism for state changes, interactive interfaces would be difficult to build.
Flutter's documentation recommends StatefulWidget when a widget's appearance or data needs to change during its lifetime. :contentReference[oaicite:2]{index=2}
5. Basic Structure of a StatefulWidget
A StatefulWidget normally consists of two classes:
- A class extending
StatefulWidget.
- A corresponding class extending
State.
class MyWidget extends StatefulWidget {
const MyWidget({super.key});
@override
State createState() => _MyWidgetState();
}
class _MyWidgetState extends State {
@override
Widget build(BuildContext context) {
return const Text('Hello');
}
}
Flutter's framework calls createState() to create the mutable state associated with the widget's location in the tree. :contentReference[oaicite:3]{index=3}
6. Understanding the Two Classes
StatefulWidget Class
class MyWidget extends StatefulWidget {
const MyWidget({super.key});
@override
State createState() => _MyWidgetState();
}
The StatefulWidget class represents the widget configuration. Its fields are normally immutable.
State Class
class _MyWidgetState extends State {
int count = 0;
@override
Widget build(BuildContext context) {
return Text('$count');
}
}
The State object contains mutable values and the build() method that describes the UI.
7. StatefulWidget Architecture
StatefulWidget
↓
createState()
↓
State Object
↓
build()
↓
Widget Tree
↓
UI
When the state changes:
State changes
↓
setState()
↓
Flutter schedules rebuild
↓
build()
↓
Updated Widget Tree
↓
Updated UI
8. Simple StatefulWidget Example
import 'package:flutter/material.dart';
class CounterPage extends StatefulWidget {
const CounterPage({super.key});
@override
State createState() => _CounterPageState();
}
class _CounterPageState extends State {
int count = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Counter'),
),
body: Center(
child: Text(
'$count',
style: const TextStyle(fontSize: 40),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
count++;
});
},
child: const Icon(Icons.add),
),
);
}
}
9. Understanding the Counter Example
The variable:
int count = 0;
stores the current state.
The button changes it:
setState(() {
count++;
});
The UI reads the value:
Text('$count')
When setState() is called, Flutter schedules the State object for rebuilding, allowing the updated value to appear in the UI. :contentReference[oaicite:4]{index=4}
10. What is setState()?
setState() is a method of the State class. It tells Flutter that internal state has changed and that the affected widget subtree should be rebuilt. :contentReference[oaicite:5]{index=5}
Basic syntax:
setState(() {
variable = newValue;
});
Example:
setState(() {
count++;
});
11. Why is setState() Important?
Changing a state variable directly does not by itself tell Flutter that the UI needs to rebuild.
For example:
count++;
The variable may change, but Flutter is not explicitly notified that the change should be reflected in the UI.
Correct approach:
setState(() {
count++;
});
Flutter's official documentation explains that changing state without calling setState() may leave the displayed UI unchanged because the framework was not notified to schedule a rebuild. :contentReference[oaicite:6]{index=6}
12. StatefulWidget Lifecycle
A StatefulWidget has an associated State object with a lifecycle.
A simplified lifecycle is:
createState()
↓
mounted
↓
initState()
↓
didChangeDependencies()
↓
build()
↓
setState()
↓
build()
↓
didUpdateWidget()
↓
build()
↓
dispose()
The exact callbacks depend on what happens to the widget in the tree, but understanding the major lifecycle methods is important for Flutter development. :contentReference[oaicite:7]{index=7}
13. createState()
createState() creates the State object associated with the StatefulWidget.
@override
State createState() => _MyWidgetState();
Flutter calls this method when it needs a State object for that widget at a particular location in the widget tree. :contentReference[oaicite:8]{index=8}
14. initState()
initState() is called when the State object is initialized. It is commonly used for one-time initialization.
@override
void initState() {
super.initState();
// Initialization code
}
Typical uses include:
- Initializing controllers
- Starting certain one-time operations
- Setting initial values
- Subscribing to services or listeners
When overriding initState(), call super.initState(). :contentReference[oaicite:9]{index=9}
15. build()
The build() method describes the UI for the current state.
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text('Hello Flutter'),
),
);
}
When state changes and setState() is used, Flutter can call build() again to reflect the new state. :contentReference[oaicite:10]{index=10}
16. dispose()
dispose() is called when the State object is permanently removed from the widget tree. It is commonly used to clean up resources.
@override
void dispose() {
controller.dispose();
super.dispose();
}
Typical cleanup tasks include:
- Disposing controllers
- Canceling timers
- Removing listeners
- Closing resources
- Unsubscribing from services
Flutter's documentation recommends cleanup in dispose() for resources such as timers and subscriptions. :contentReference[oaicite:11]{index=11}
17. Complete Lifecycle Example
class ExamplePage extends StatefulWidget {
const ExamplePage({super.key});
@override
State createState() => _ExamplePageState();
}
class _ExamplePageState extends State {
@override
void initState() {
super.initState();
print('initState called');
}
@override
Widget build(BuildContext context) {
print('build called');
return const Scaffold(
body: Center(
child: Text('Hello Flutter'),
),
);
}
@override
void dispose() {
print('dispose called');
super.dispose();
}
}
18. StatefulWidget with Boolean State
A Boolean variable is useful for representing two states such as true/false, enabled/disabled, or visible/hidden.
class ToggleExample extends StatefulWidget {
const ToggleExample({super.key});
@override
State createState() => _ToggleExampleState();
}
class _ToggleExampleState extends State {
bool isActive = false;
@override
Widget build(BuildContext context) {
return Switch(
value: isActive,
onChanged: (value) {
setState(() {
isActive = value;
});
},
);
}
}
19. StatefulWidget with Checkbox
class CheckboxExample extends StatefulWidget {
const CheckboxExample({super.key});
@override
State createState() => _CheckboxExampleState();
}
class _CheckboxExampleState extends State {
bool accepted = false;
@override
Widget build(BuildContext context) {
return Checkbox(
value: accepted,
onChanged: (value) {
setState(() {
accepted = value ?? false;
});
},
);
}
}
The checkbox state is stored in accepted.
20. StatefulWidget with Slider
class SliderExample extends StatefulWidget {
const SliderExample({super.key});
@override
State createState() => _SliderExampleState();
}
class _SliderExampleState extends State {
double value = 50;
@override
Widget build(BuildContext context) {
return Slider(
value: value,
min: 0,
max: 100,
onChanged: (newValue) {
setState(() {
value = newValue;
});
},
);
}
}
21. StatefulWidget with TextField
Text input often involves changing values during user interaction.
class NameInput extends StatefulWidget {
const NameInput({super.key});
@override
State createState() => _NameInputState();
}
class _NameInputState extends State {
String name = '';
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
onChanged: (value) {
setState(() {
name = value;
});
},
),
Text('Hello $name'),
],
);
}
}
Each time the text changes, the state can be updated and the UI can rebuild.
22. StatefulWidget for Show and Hide UI
class VisibilityExample extends StatefulWidget {
const VisibilityExample({super.key});
@override
State createState() => _VisibilityExampleState();
}
class _VisibilityExampleState extends State {
bool showMessage = true;
@override
Widget build(BuildContext context) {
return Column(
children: [
if (showMessage)
const Text('Welcome to Flutter'),
ElevatedButton(
onPressed: () {
setState(() {
showMessage = !showMessage;
});
},
child: const Text('Toggle'),
),
],
);
}
}
23. StatefulWidget for Dynamic Color
class ColorExample extends StatefulWidget {
const ColorExample({super.key});
@override
State createState() => _ColorExampleState();
}
class _ColorExampleState extends State {
Color boxColor = Colors.blue;
void changeColor() {
setState(() {
boxColor = boxColor == Colors.blue
? Colors.green
: Colors.blue;
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Container(
width: 150,
height: 150,
color: boxColor,
),
ElevatedButton(
onPressed: changeColor,
child: const Text('Change Color'),
),
],
);
}
}
24. StatefulWidget for Favorite Button
class FavoriteButton extends StatefulWidget {
const FavoriteButton({super.key});
@override
State createState() => _FavoriteButtonState();
}
class _FavoriteButtonState extends State {
bool isFavorite = false;
@override
Widget build(BuildContext context) {
return IconButton(
icon: Icon(
isFavorite
? Icons.favorite
: Icons.favorite_border,
),
onPressed: () {
setState(() {
isFavorite = !isFavorite;
});
},
);
}
}
25. StatefulWidget for Tab Selection
class TabExample extends StatefulWidget {
const TabExample({super.key});
@override
State createState() => _TabExampleState();
}
class _TabExampleState extends State {
int selectedIndex = 0;
@override
Widget build(BuildContext context) {
return Row(
children: [
ElevatedButton(
onPressed: () {
setState(() {
selectedIndex = 0;
});
},
child: Text(
selectedIndex == 0 ? 'Selected' : 'Tab 1',
),
),
ElevatedButton(
onPressed: () {
setState(() {
selectedIndex = 1;
});
},
child: Text(
selectedIndex == 1 ? 'Selected' : 'Tab 2',
),
),
],
);
}
}
26. Multiple State Variables
A StatefulWidget can manage multiple state variables.
class ProfileSettings extends StatefulWidget {
const ProfileSettings({super.key});
@override
State createState() => _ProfileSettingsState();
}
class _ProfileSettingsState extends State {
bool notifications = true;
bool darkMode = false;
String username = 'Manish';
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(username),
Switch(
value: notifications,
onChanged: (value) {
setState(() {
notifications = value;
});
},
),
Switch(
value: darkMode,
onChanged: (value) {
setState(() {
darkMode = value;
});
},
),
],
);
}
}
27. StatefulWidget and Conditional UI
State can determine which widget should be displayed.
bool isLoggedIn = true;
@override
Widget build(BuildContext context) {
return isLoggedIn
? const Text('Welcome User')
: const Text('Please Login');
}
The state can be changed using:
setState(() {
isLoggedIn = !isLoggedIn;
});
28. StatefulWidget and Loading State
A common real-world use of StatefulWidget is displaying a loading indicator while an operation is running.
class LoadingExample extends StatefulWidget {
const LoadingExample({super.key});
@override
State createState() => _LoadingExampleState();
}
class _LoadingExampleState extends State {
bool isLoading = false;
Future loadData() async {
setState(() {
isLoading = true;
});
await Future.delayed(
const Duration(seconds: 2),
);
if (!mounted) return;
setState(() {
isLoading = false;
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
if (isLoading)
const CircularProgressIndicator()
else
const Text('Data Loaded'),
ElevatedButton(
onPressed: isLoading ? null : loadData,
child: const Text('Load'),
),
],
);
}
}
When asynchronous work finishes, make sure the State object is still mounted before calling setState(). Flutter also recommends canceling work that could trigger updates after disposal when possible. :contentReference[oaicite:12]{index=12}
29. StatefulWidget and Form Validation
Stateful widgets can be used to manage form-related UI state.
class LoginForm extends StatefulWidget {
const LoginForm({super.key});
@override
State createState() => _LoginFormState();
}
class _LoginFormState extends State {
String email = '';
String password = '';
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
onChanged: (value) {
setState(() {
email = value;
});
},
),
TextField(
obscureText: true,
onChanged: (value) {
setState(() {
password = value;
});
},
),
ElevatedButton(
onPressed: email.isNotEmpty && password.isNotEmpty
? () {}
: null,
child: const Text('Login'),
),
],
);
}
}
30. StatefulWidget and Parent-Child State Management
Sometimes a parent widget should manage state used by one or more child widgets.
ParentWidget
├── ChildWidget
├── ChildWidget
└── ChildWidget
The parent can maintain shared state and pass values to children through constructors.
ChildWidget(
isActive: isActive,
onChanged: updateState,
)
This approach is commonly called lifting state up: the state is moved to the lowest common ancestor that needs to coordinate the relevant widgets. Flutter's interactivity documentation describes parent-managed state as one common state-management approach. :contentReference[oaicite:13]{index=13}
31. StatefulWidget and Callback Communication
A child widget can notify a parent using a callback.
class ChildWidget extends StatelessWidget {
const ChildWidget({
required this.onChanged,
super.key,
});
final ValueChanged onChanged;
@override
Widget build(BuildContext context) {
return Switch(
value: true,
onChanged: onChanged,
);
}
}
Parent:
ChildWidget(
onChanged: (value) {
setState(() {
isActive = value;
});
},
)
32. StatefulWidget and const
The StatefulWidget class itself can have a const constructor when its configuration permits it.
class CounterPage extends StatefulWidget {
const CounterPage({super.key});
@override
State createState() => _CounterPageState();
}
The mutable values belong in the State object rather than the widget configuration.
33. StatefulWidget Does Not Mean Every Variable Must Be State
Not every variable inside a State class needs to change.
Only values whose changes should affect the widget's behavior or appearance need to be treated as mutable state.
class Example extends StatefulWidget {
const Example({super.key});
@override
State createState() => _ExampleState();
}
class _ExampleState extends State {
int count = 0;
final String title = 'Counter';
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(title),
Text('$count'),
],
);
}
}
34. StatefulWidget and Widget Properties
Suppose a parent provides a title to a StatefulWidget.
class ProfilePage extends StatefulWidget {
const ProfilePage({
required this.title,
super.key,
});
final String title;
@override
State createState() => _ProfilePageState();
}
The State object can access the current widget configuration using the widget property.
class _ProfilePageState extends State {
@override
Widget build(BuildContext context) {
return Text(widget.title);
}
}
When the parent rebuilds with a new configuration for the same location, Flutter can update the State object's widget reference while retaining the State object when appropriate. :contentReference[oaicite:14]{index=14}
35. didUpdateWidget()
didUpdateWidget() can be overridden when a StatefulWidget receives a new widget configuration while its State object is retained.
@override
void didUpdateWidget(covariant ProfilePage oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.title != widget.title) {
// React to changed configuration.
}
}
This is useful when the State object needs to respond to changes in widget properties. The State lifecycle includes this callback as part of updates to an existing State object. :contentReference[oaicite:15]{index=15}
36. didChangeDependencies()
didChangeDependencies() is called when an inherited dependency used by the State changes.
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Respond to dependency changes.
}
It is also called after initState(). :contentReference[oaicite:16]{index=16}
37. mounted Property
The mounted property indicates whether the State object is currently associated with an element in the widget tree.
This is particularly useful after asynchronous operations:
Future loadData() async {
await Future.delayed(
const Duration(seconds: 2),
);
if (!mounted) return;
setState(() {
// Update state.
});
}
Calling setState() after the State object has been disposed is an error. :contentReference[oaicite:17]{index=17}
38. setState() Should Not Be Async
The callback passed to setState() should perform the synchronous state update and should not itself be async.
Incorrect:
setState(() async {
await loadData();
count++;
});
Better:
await loadData();
if (!mounted) return;
setState(() {
count++;
});
The Flutter API documentation explicitly states that the callback passed to setState() must not return a Future. :contentReference[oaicite:18]{index=18}
39. Common Mistake: Forgetting setState()
Incorrect:
void increment() {
count++;
}
Correct:
void increment() {
setState(() {
count++;
});
}
40. Common Mistake: Calling setState() Inside build()
Avoid calling setState() directly from build().
Incorrect:
@override
Widget build(BuildContext context) {
setState(() {
count++;
});
return Text('$count');
}
This can create unnecessary rebuild cycles and should be avoided.
41. Common Mistake: Heavy Work Inside setState()
Only the actual state mutation should generally be placed inside setState().
Instead of:
setState(() {
performExpensiveCalculation();
count++;
saveDataToDisk();
});
Prefer:
performExpensiveCalculation();
setState(() {
count++;
});
saveDataToDisk();
Flutter's API documentation recommends keeping setState() focused on the state change rather than unrelated computation or side effects. :contentReference[oaicite:19]{index=19}
42. Common Mistake: Calling setState() After dispose()
Incorrect:
Future loadData() async {
await Future.delayed(
const Duration(seconds: 3),
);
setState(() {
dataLoaded = true;
});
}
If the widget has been removed during the delay, the State object may no longer be mounted.
Safer pattern:
Future loadData() async {
await Future.delayed(
const Duration(seconds: 3),
);
if (!mounted) return;
setState(() {
dataLoaded = true;
});
}
Whenever possible, cancel work that can trigger updates after disposal rather than relying only on a mounted check. :contentReference[oaicite:20]{index=20}
43. StatefulWidget with Timer
import 'dart:async';
import 'package:flutter/material.dart';
class TimerExample extends StatefulWidget {
const TimerExample({super.key});
@override
State createState() => _TimerExampleState();
}
class _TimerExampleState extends State {
int seconds = 0;
Timer? timer;
@override
void initState() {
super.initState();
timer = Timer.periodic(
const Duration(seconds: 1),
(_) {
if (!mounted) return;
setState(() {
seconds++;
});
},
);
}
@override
void dispose() {
timer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Text('$seconds seconds');
}
}
The timer is started during initialization and canceled during disposal to avoid continuing work after the widget is removed.
44. StatefulWidget with TextEditingController
Controllers are commonly used with input widgets and should be disposed when no longer needed.
class SearchBox extends StatefulWidget {
const SearchBox({super.key});
@override
State createState() => _SearchBoxState();
}
class _SearchBoxState extends State {
final TextEditingController controller =
TextEditingController();
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return TextField(
controller: controller,
);
}
}
45. StatefulWidget and AnimationController
Some stateful features require controllers that need lifecycle management.
class AnimatedBox extends StatefulWidget {
const AnimatedBox({super.key});
@override
State createState() => _AnimatedBoxState();
}
class _AnimatedBoxState
extends State
with SingleTickerProviderStateMixin {
late AnimationController controller;
@override
void initState() {
super.initState();
controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return const SizedBox();
}
}
The key lesson is that resources created by a State object should be cleaned up when the State is disposed.
46. Complete Practical Example
import 'package:flutter/material.dart';
void main() {
runApp(const MaterialApp(
home: CounterPage(),
));
}
class CounterPage extends StatefulWidget {
const CounterPage({super.key});
@override
State createState() => _CounterPageState();
}
class _CounterPageState extends State {
int count = 0;
bool isActive = false;
void increment() {
setState(() {
count++;
});
}
void decrement() {
setState(() {
count--;
});
}
void toggleActive() {
setState(() {
isActive = !isActive;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Stateful Widget Demo'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Count: $count',
style: const TextStyle(
fontSize: 28,
),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
onPressed: decrement,
icon: const Icon(Icons.remove),
),
IconButton(
onPressed: increment,
icon: const Icon(Icons.add),
),
],
),
const SizedBox(height: 20),
Text(
isActive ? 'Active' : 'Inactive',
style: TextStyle(
color: isActive
? Colors.green
: Colors.red,
),
),
Switch(
value: isActive,
onChanged: (_) {
toggleActive();
},
),
],
),
),
);
}
}
What This Example Demonstrates
- Creating a StatefulWidget.
- Creating a State class.
- Using mutable state.
- Updating an integer with
setState().
- Updating Boolean state.
- Dynamically changing displayed text.
- Dynamically changing text color.
- Using a Switch.
- Using multiple state variables.
47. Widget Tree for the Practical Example
MaterialApp
└── CounterPage
└── Scaffold
├── AppBar
│ └── Text
└── Center
└── Column
├── Text
├── Row
│ ├── IconButton
│ │ └── Icon
│ └── IconButton
│ └── Icon
├── Text
└── Switch
48. Real-World Uses of StatefulWidget
| Use Case |
Example State |
| Counter |
Current count |
| Login Form |
Email, password, validation status |
| Shopping Cart |
Item quantity |
| Favorite Button |
Favorite or not favorite |
| Settings |
Enabled/disabled options |
| Slider |
Current slider value |
| Search |
Search query |
| Loading Screen |
Loading or completed |
| Timer |
Elapsed time |
| Form |
Input and validation state |
| Tab Interface |
Selected tab |
| Animation |
Animation progress |
49. When Should You Use StatefulWidget?
Use a StatefulWidget when a widget's appearance or data needs to change during its lifetime and that state is appropriate to manage locally in its State object.
Typical situations include:
- User interactions change the UI.
- A value changes over time.
- The widget needs local mutable state.
- A controller needs to be managed with the widget lifecycle.
- The UI needs to rebuild after a local state change.
However, not every dynamic application feature requires local StatefulWidget state. Flutter also supports broader state-management approaches for state shared across multiple parts of an application. :contentReference[oaicite:21]{index=21}
50. When Should You Prefer StatelessWidget?
Use a StatelessWidget when the widget's UI can be completely described by its immutable configuration and the current context without maintaining mutable local state.
Example:
class WelcomeText extends StatelessWidget {
const WelcomeText({super.key});
@override
Widget build(BuildContext context) {
return const Text(
'Welcome to Flutter',
);
}
}
The text does not need to maintain changing local state, so a StatelessWidget is appropriate.
51. StatefulWidget Best Practices
- Keep the State class focused on the state it actually manages.
- Use
setState() only when a state change should affect the UI.
- Keep the
setState() callback small.
- Do not perform expensive operations inside
setState().
- Dispose controllers and subscriptions when necessary.
- Cancel timers and other ongoing work when the State is disposed.
- Check
mounted before updating state after asynchronous work when appropriate.
- Break large widgets into smaller reusable widgets.
- Use
const constructors where appropriate.
- Move shared state to an appropriate parent or state-management solution when multiple widgets need the same state.
52. Common Mistakes Summary
| Mistake |
Better Approach |
| Changing state without setState() |
Use setState() for UI-affecting local state changes |
| Calling setState() inside build() |
Trigger state changes from events or lifecycle logic |
| Making setState() callback async |
Await asynchronous work first, then call setState() |
| Calling setState() after dispose() |
Cancel work or check mounted |
| Forgetting to dispose controllers |
Dispose controllers in dispose() |
| Putting unrelated work inside setState() |
Keep only the actual state mutation inside |
| Using StatefulWidget for everything |
Use StatelessWidget when local mutable state is unnecessary |
53. Interview Questions
Q1. What is a StatefulWidget?
A StatefulWidget is a widget whose associated State object can hold mutable data that may change during the widget's lifetime.
Q2. Is StatefulWidget itself mutable?
No. The StatefulWidget is immutable. Mutable state is stored in its associated State object. :contentReference[oaicite:22]{index=22}
Q3. Why does StatefulWidget require two classes?
The StatefulWidget represents immutable configuration, while the State object stores mutable state and implements the build method.
Q4. What does createState() do?
It creates the State object associated with a StatefulWidget at a particular location in the widget tree. :contentReference[oaicite:23]{index=23}
Q5. What is setState()?
It notifies Flutter that the State object's internal state has changed and that the relevant UI should be rebuilt. :contentReference[oaicite:24]{index=24}
Q6. What happens if you change state without setState()?
The internal value may change, but Flutter may not rebuild the affected UI to display the new value.
Q7. What is initState() used for?
It is commonly used for one-time initialization when the State object is first created.
Q8. What is dispose() used for?
It is used to clean up resources such as controllers, timers, and subscriptions before the State object is permanently removed.
Q9. Can setState() be async?
No. The callback passed to setState() should not return a Future. Perform asynchronous work separately and then synchronously update state inside setState(). :contentReference[oaicite:25]{index=25}
Q10. What is mounted?
mounted indicates whether the State object is currently associated with an active element in the widget tree.
54. Practice Exercise
Create a StatefulWidget called ShoppingCart with the following functionality:
- Display a product name.
- Display the product price.
- Create a quantity variable initialized to
1.
- Add a plus button.
- Add a minus button.
- Update quantity using
setState().
- Prevent quantity from becoming less than
1.
- Calculate the total price dynamically.
- Display the total price.
Expected Flow
Initial Quantity = 1
↓
User presses +
↓
setState()
↓
Quantity = 2
↓
Total Price Updates
↓
UI Rebuilds
55. Quick Revision Table
| Concept |
Meaning |
| StatefulWidget |
Widget used when associated state can change over time |
| State |
Mutable information associated with a StatefulWidget |
| createState() |
Creates the State object |
| initState() |
Used for one-time initialization |
| build() |
Describes the current UI |
| setState() |
Notifies Flutter about a state change and schedules rebuilding |
| didUpdateWidget() |
Responds to updated widget configuration |
| didChangeDependencies() |
Responds to changes in inherited dependencies |
| mounted |
Indicates whether State is currently mounted |
| dispose() |
Cleans up resources before State is permanently removed |
56. Key Takeaways
- StatefulWidget is used for UI that can change during its lifetime.
- A StatefulWidget has a separate State object.
- The StatefulWidget itself is immutable.
- Mutable values are generally stored in the State object.
createState() creates the associated State object.
build() describes the UI for the current state.
setState() tells Flutter that a state change may affect the UI.
initState() is useful for one-time initialization.
dispose() is used for cleanup.
- Controllers, timers, and subscriptions should be managed according to their lifecycle.
- After asynchronous work, make sure the State is still mounted before updating it.
- State can be managed locally or moved to an appropriate parent or broader state-management solution.
57. Learning Resources
Conclusion
Stateful Widgets are essential for building interactive Flutter applications. They allow developers to manage values that change during the lifetime of a widget, such as counters, form inputs, selections, loading states, timers, and interactive controls. The key idea is that the StatefulWidget provides immutable configuration while its associated State object stores mutable data. When that data changes, setState() notifies Flutter so the relevant UI can rebuild. Understanding StatefulWidget, State, createState(), initState(), build(), setState(), mounted, and dispose() provides a strong foundation for developing interactive Flutter applications.