Module 1
Topic 2

Local State

Managing ephemeral state with StatefulWidget and setState() β€” the foundation of interactive Flutter apps.

Flutter Docs – Interactivity & State Ephemeral vs App State Flutter Dev – Widget State Explained
Recap: What Is Local State?

In the previous topic, we learned that state can be split into two types: ephemeral state (also called local or UI state) and app state .

πŸ“¦ Local (Ephemeral) State

Local state is state that is neatly contained in a single widget and doesn't need to be shared with other parts of your app. It's temporary, local to a specific widget subtree, and doesn't need to be persisted across app restarts.

  • βœ“ Managed with StatefulWidget and setState()
  • βœ“ Perfect for UI interactions like toggles, animations, and form inputs
  • βœ“ No need for complex state management solutions
StatefulWidget: The Container for Local State

A StatefulWidget is a widget that can change its appearance in response to events triggered by user interactions or data changes. It does this by maintaining a separate State object that holds the mutable state.

βœ… Two Classes, One Purpose

Every StatefulWidget comes in two parts: the widget class (immutable, defines the configuration) and the state class (mutable, holds the state and builds the UI). The framework manages the state object and calls build() whenever the state changes.

setState(): The Heart of Local State

setState() is the method you call to tell the framework that the state of your widget has changed. When you call setState() , two things happen:

  • 1. The code inside the callback runs (where you update your state variables)
  • 2. The framework schedules a rebuild of the widget (calls build() again)

⚠️ Critical Rule

Only call setState() inside the State class. Never call it from outside the State class. Also, never call setState() in initState() or build() β€” this will cause infinite rebuild loops.

How setState() Works

πŸ”„ The Rebuild Flow

  • User interaction triggers an event (e.g., button tap)
  • Event handler calls setState() with a callback
  • Inside the callback: you update the state variables
  • After the callback: the framework marks the widget as "dirty"
  • Next frame: the framework calls build() to rebuild the widget with the new state
  • UI updates to reflect the new state
Complete Example: A Simple Counter

Let's look at a complete, runnable example of a counter app that uses local state. This is the classic "Hello World" of state management.

Step-by-Step Explanation
1.
The StatefulWidget class – CounterScreen extends StatefulWidget . It's the immutable configuration of the widget.
2.
createState() – This method returns the state object ( _CounterScreenState ) that will manage the mutable state for this widget.
3.
The State class – _CounterScreenState holds the mutable state. The underscore ( _ ) makes it private to the library.
4.
State field – _counter is the local state. It's initialized to 0 and changed only inside setState() callbacks.
5.
Event handlers – _incrementCounter , _decrementCounter , and _resetCounter wrap their state changes in setState() .
6.
build() method – Uses _counter to display the current count. When setState() is called, this method is re‑executed with the updated _counter value.
The Lifecycle of a StatefulWidget

Understanding the lifecycle of a StatefulWidget is crucial for managing resources and avoiding bugs. Here are the most important lifecycle methods:

πŸ”„
createState()
Called when the widget is inserted into the tree
πŸš€
initState()
Called once when the state object is created
πŸ“¦
didChangeDependencies()
Called when dependencies change
πŸ–ŒοΈ
build()
Called every time the widget needs to rebuild
♻️
didUpdateWidget()
Called when parent widget rebuilds with new config
πŸ—‘οΈ
dispose()
Called when the state object is removed from the tree

⚠️ Important: initState() and dispose()

Always override initState() and dispose() in pairs. Use initState() to initialize controllers, listeners, or animations. Use dispose() to clean them up and prevent memory leaks. Never call setState() in initState() β€” the widget hasn't been built yet.

Common Mistakes
❌ Mistake 1: Calling setState() outside the State class

You cannot call setState() from outside the State class. Keep all state logic inside the State class.

βœ… Correct: Call setState() inside the State class

All state changes should be triggered from within the State class, typically in event handlers.

❌ Mistake 2: Using setState() for app-wide state

If multiple widgets need access to the same data, that's app state , not local state. Using setState() for app state leads to prop drilling and hard-to-maintain code.

βœ… Correct: Use state management for app state

For state that needs to be shared across widgets, use a state management solution like Riverpod, Provider, or BLoC. We'll cover this in later topics.

❌ Mistake 3: Calling setState() in initState() or build()

This causes an infinite rebuild loop. The widget rebuilds, which triggers setState() , which triggers another rebuild, and so on.

βœ… Correct: Use initState() for initialization, not state changes

Use initState() to initialize controllers, streams, or animations. Use event handlers (like button callbacks) to call setState() .

When to Use Local State vs App State

βœ… Use Local State When

  • The state is only needed within a single widget
  • No other widget needs to know about the state
  • The state is temporary (e.g., animation progress, toggle state)
  • You don't need to persist the state between sessions

Example: Checkbox state, TabController index, TextField input

πŸ”€ Use App State When

  • Multiple widgets need access to the same data
  • State needs to be persisted across sessions
  • State changes need to update multiple parts of the UI
  • The state is complex and requires structured management

Example: User authentication, shopping cart, app settings

πŸ’‘ Pro Tip: Start with Local State, Evolve When Needed

It's perfectly fine to start with StatefulWidget and setState() . When you find yourself passing callbacks down multiple levels or duplicating state, it's time to consider a state management solution. Don't over-engineer prematurely.

🎯 Key Takeaway

Local state is managed with StatefulWidget and setState() . It's perfect for UI interactions that don't need to be shared across widgets. Use it for what it's good for, and reach for a state management solution when your app grows. When in doubt, ask: "Does another widget need to know about this state?"