Flutter’s declarative framework makes building cross-platform apps fast and efficient, but for many developers—especially beginners—understanding how state changes trigger UI rebuilds can be confusing. A widget’s UI is only an output of its current state, and whenever the state changes, Flutter intelligently rebuilds the necessary parts of the widget tree.
In this comprehensive post, we will explore the concept of state vs UI rebuilds, how Flutter manages state changes, how widgets rebuild, what actually gets redrawn, and how to optimize rebuilds for performance.
This deep dive will include:
- What state means in Flutter
- How state is different from UI
- Why Flutter rebuilds the UI on state change
- The rebuild lifecycle in Flutter
- Examples of rebuilding in action
- Optimizing rebuilds for performance
- Best practices for managing state and rebuilds
Understanding State in Flutter
State in Flutter refers to data that changes over time and can affect how a widget looks or behaves.
Two Types of State
- Ephemeral (Local) State
- Exists only within a single widget.
- Example: Whether a checkbox is checked or unchecked.
- Managed using
setState.
- App (Global) State
- Shared across multiple parts of the app.
- Example: Authentication status of a user or contents of a shopping cart.
- Managed using state management solutions like
Provider,Riverpod, orBloc.
State acts as the source of truth for the UI. The UI does not store its own information—it simply reflects whatever the state currently is.
What is UI in Flutter?
UI (User Interface) in Flutter is defined by widgets. Every element on the screen, from a text label to a complex page layout, is built from widgets.
Widgets in Flutter are:
- Immutable: Once created, they cannot change.
- Declarative: The UI is rebuilt from scratch when the state changes.
This immutability is the reason why Flutter rebuilds widgets instead of modifying them directly.
Why State and UI Are Connected
The core principle of Flutter is:
UI = Function(State)
This means the user interface is a direct reflection of the current state.
For example:
int counter = 0;
Text('Counter: $counter');
If counter changes from 0 to 1, the Text widget displaying the counter must also change. Flutter achieves this by rebuilding the Text widget with the updated value.
The Role of setState in Rebuilding
For StatefulWidget, you explicitly tell Flutter that something has changed by calling setState.
setState(() {
counter++;
});
- The state variable (
counter) is updated. - Flutter schedules a rebuild for the widget.
- The
buildmethod runs again, constructing the updated UI tree.
This is why state and UI are deeply tied: the UI will not change unless the framework is told the state has changed.
How Flutter Rebuilds Widgets
Flutter’s rebuild mechanism follows these steps:
- You update the state
- Using
setStateor another state management trigger.
- Using
- Framework schedules a rebuild
- Only for the widget where the state changed.
buildmethod is called- The widget tree is reconstructed with the new state values.
- Diffing and Element Tree Update
- Flutter compares the new widget tree with the old one.
- If a widget has the same type and key, Flutter reuses the existing element.
- If different, Flutter replaces it.
- UI Updates on Screen
- Only the parts of the UI that actually changed are redrawn.
Example: Counter App and Rebuilds
Here’s a simple example:
class CounterScreen extends StatefulWidget {
@override
_CounterScreenState createState() => _CounterScreenState();
}
class _CounterScreenState extends State<CounterScreen> {
int _counter = 0;
void _increment() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
print("Build method called");
return Scaffold(
appBar: AppBar(title: Text('Counter Example')),
body: Center(
child: Text('Counter: $_counter', style: TextStyle(fontSize: 24)),
),
floatingActionButton: FloatingActionButton(
onPressed: _increment,
child: Text('+'),
),
);
}
}
- Every time the button is pressed,
_counterchanges. setStatenotifies Flutter.- The
buildmethod runs again, updating theTextwidget. - You will see “Build method called” printed each time.
What Actually Gets Rebuilt?
A common misconception is that Flutter redraws the entire screen every time state changes. This is not true.
Flutter only rebuilds:
- The widget where
setStatewas called. - Its descendants, if they depend on the changed state.
Thanks to Flutter’s widget tree and Element Tree reconciliation, widgets that haven’t changed are reused.
For example, in the counter app above:
- The
AppBardoes not rebuild when the counter changes. - Only the
Textwidget displaying the counter is updated.
Stateless vs Stateful Widgets and Rebuilds
StatelessWidget
- Has no internal state.
- UI is fixed unless rebuilt by a parent widget.
StatefulWidget
- Maintains internal state using
State<T>. - UI updates when
setStateis called.
Deep Dive: Widget, Element, and RenderObject Trees
To fully understand rebuilds, you must know about Flutter’s three trees:
- Widget Tree
- Blueprint of the UI.
- Immutable and lightweight.
- Element Tree
- Links widgets to the render tree.
- Maintains the relationship between widgets and their position in the tree.
- RenderObject Tree
- Handles the actual layout, painting, and hit-testing.
When state changes:
- The Widget Tree is rebuilt.
- The Element Tree decides which widgets can be reused.
- The Render Tree only updates the affected parts.
This makes rebuilds very efficient.
Example: Partial Rebuilds
Consider this widget:
Column(
children: [
Text('Static Text'),
Text('Counter: $_counter'),
],
);
- When
_counterchanges, only the secondTextwidget rebuilds. - The first
Textwidget remains untouched because it has not changed.
Rebuilds and Keys
Keys in Flutter help the framework decide whether to reuse or recreate widgets during rebuilds.
For example:
ListView(
children: items.map((item) => ListTile(
key: ValueKey(item.id),
title: Text(item.name),
)).toList(),
);
Without keys, Flutter may rebuild the entire list when an item changes. With keys, it reuses widgets efficiently.
Performance Considerations in Rebuilds
Rebuilding is efficient in Flutter, but excessive rebuilds can cause performance issues.
Tips to Optimize:
- Extract Widgets
- Break down large widgets into smaller widgets.
- Prevents unnecessary rebuilds of unrelated UI parts.
- Use const Widgets
- Declare widgets as
constwhen possible. - Flutter does not rebuild
constwidgets because they are immutable.
- Declare widgets as
- Use Keys
- Help Flutter differentiate between widgets and reuse them.
- Avoid Heavy Computation in build()
- Keep the
buildmethod light. - Perform complex logic outside the build and only pass results.
- Keep the
- Memoization Techniques
- Cache computed results instead of recalculating on every rebuild.
Common Mistakes with Rebuilds
- Calling setState unnecessarily
- Leads to unnecessary rebuilds.
- Only call when actual state changes.
- Heavy work inside setState
- Slows down rebuilds.
- Calling setState inside build
- Causes infinite rebuild loops.
- Forgetting about mounted property in async calls
- Can cause errors if a widget is disposed before setState is called.
Debugging Rebuilds
- Print Statements
- Add logs inside
build()to see when it is called.
- Add logs inside
- Flutter DevTools
- Inspect the widget tree and track rebuilds.
- Repaint Rainbow
- In Flutter’s performance overlay, enables visual debugging of repaint areas.
Advanced Topics
1. InheritedWidget and Rebuilds
InheritedWidget allows data to propagate down the widget tree. Widgets that depend on inherited data rebuild automatically when the data changes.
2. Provider and Rebuilds
Provider optimizes rebuilds by only rebuilding consumers of data, not the entire tree.
3. Bloc and State Streams
Bloc uses streams and events to update state, ensuring precise control over rebuilds.
Practical Example: Todo App and Rebuilds
class TodoList extends StatefulWidget {
@override
_TodoListState createState() => _TodoListState();
}
class _TodoListState extends State<TodoList> {
List<String> _todos = [];
void _addTodo() {
setState(() {
_todos.add('Task ${_todos.length + 1}');
});
}
@override
Widget build(BuildContext context) {
print('TodoList rebuilt');
return Scaffold(
appBar: AppBar(title: Text('Todo List')),
body: ListView.builder(
itemCount: _todos.length,
itemBuilder: (context, index) {
print('List item $index rebuilt');
return ListTile(title: Text(_todos[index]));
},
),
floatingActionButton: FloatingActionButton(
onPressed: _addTodo,
child: Text('+'),
),
);
}
}
Observation:
- Adding a new todo rebuilds the entire
ListView. - However, Flutter reuses existing list items because of the Element tree.
- Only new items are created, making the process efficient.
Best Practices for State and UI Rebuilds
- Keep state local if possible.
- Avoid rebuilding large trees unnecessarily.
- Use
constwidgets generously. - Leverage keys to preserve widget identity.
- Extract smaller widgets to isolate rebuilds.
- Use advanced state management when state grows complex.
Leave a Reply