State vs UI Rebuilds in Flutter

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

  1. Ephemeral (Local) State
    • Exists only within a single widget.
    • Example: Whether a checkbox is checked or unchecked.
    • Managed using setState.
  2. 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, or Bloc.

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 build method 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:

  1. You update the state
    • Using setState or another state management trigger.
  2. Framework schedules a rebuild
    • Only for the widget where the state changed.
  3. build method is called
    • The widget tree is reconstructed with the new state values.
  4. 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.
  5. 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, _counter changes.
  • setState notifies Flutter.
  • The build method runs again, updating the Text widget.
  • 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:

  1. The widget where setState was called.
  2. 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 AppBar does not rebuild when the counter changes.
  • Only the Text widget 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 setState is called.

Deep Dive: Widget, Element, and RenderObject Trees

To fully understand rebuilds, you must know about Flutter’s three trees:

  1. Widget Tree
    • Blueprint of the UI.
    • Immutable and lightweight.
  2. Element Tree
    • Links widgets to the render tree.
    • Maintains the relationship between widgets and their position in the tree.
  3. 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 _counter changes, only the second Text widget rebuilds.
  • The first Text widget 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:

  1. Extract Widgets
    • Break down large widgets into smaller widgets.
    • Prevents unnecessary rebuilds of unrelated UI parts.
  2. Use const Widgets
    • Declare widgets as const when possible.
    • Flutter does not rebuild const widgets because they are immutable.
  3. Use Keys
    • Help Flutter differentiate between widgets and reuse them.
  4. Avoid Heavy Computation in build()
    • Keep the build method light.
    • Perform complex logic outside the build and only pass results.
  5. Memoization Techniques
    • Cache computed results instead of recalculating on every rebuild.

Common Mistakes with Rebuilds

  1. Calling setState unnecessarily
    • Leads to unnecessary rebuilds.
    • Only call when actual state changes.
  2. Heavy work inside setState
    • Slows down rebuilds.
  3. Calling setState inside build
    • Causes infinite rebuild loops.
  4. Forgetting about mounted property in async calls
    • Can cause errors if a widget is disposed before setState is called.

Debugging Rebuilds

  1. Print Statements
    • Add logs inside build() to see when it is called.
  2. Flutter DevTools
    • Inspect the widget tree and track rebuilds.
  3. 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&#91;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

  1. Keep state local if possible.
  2. Avoid rebuilding large trees unnecessarily.
  3. Use const widgets generously.
  4. Leverage keys to preserve widget identity.
  5. Extract smaller widgets to isolate rebuilds.
  6. Use advanced state management when state grows complex.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *