DEV Community

Anton Samoylov
Anton Samoylov

Posted on

Flutter Anti-Pattern: How setState() Turns Your App Into a Slideshow

Flutter Anti-Pattern

The Problem: Many Flutter developers overuse setState(), calling it even when variable changes don't affect the UI. Every unnecessary setState() is a potential drop from 60 FPS to 59 FPS in complex interfaces.

Chapter 1: When setState Is Actually Unnecessary

Let’s examine a classic example found in 80% of applications: data entry forms. Developers often write:

class MyForm extends StatefulWidget {
  @override
  _MyFormState createState() => _MyFormState();
}

class _MyFormState extends State<MyForm> {
  String _username = '';
  String _password = '';

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(
          onChanged: (value) {
            setState(() {          // ❌ UNNECESSARY!
              _username = value;   // This variable is NOT used in UI
            });
          },
        ),
        TextField(
          onChanged: (value) {
            setState(() {          // ❌ UNNECESSARY!
              _password = value;   // This variable is also NOT in UI
            });
          },
        ),
        ElevatedButton(
          onPressed: () {
            // Send data to server
            _sendToServer(_username, _password);
          },
          child: Text('Submit'),
        ),
      ],
    );
  }

  void _sendToServer(String user, String pass) {
    // Send data...
  }
}
Enter fullscreen mode Exit fullscreen mode

What’s wrong here? Every character typed triggers a complete widget rebuild, even though nothing visually changes! The user sees just the cursor blinking, but under the hood Flutter rebuilds the entire widget tree.
The right approach: If data is only needed for logic or server submission — store it without UI binding:

class _MyFormState extends State<MyForm> {
  // These variables are NOT used in build()
  final TextEditingController _usernameController = TextEditingController();
  final TextEditingController _passwordController = TextEditingController();

  @override
  void initState() {
    super.initState();
    // You can add listeners for additional logic
    _usernameController.addListener(() {
      print('Username changed: ${_usernameController.text}');
      // You can do validation, debounce, etc. here
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(controller: _usernameController),
        TextField(controller: _passwordController),
        ElevatedButton(
          onPressed: () {
            // Take data directly from controllers
            _sendToServer(
              _usernameController.text,
              _passwordController.text,
            );
          },
          child: Text('Submit'),
        ),
      ],
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Key principle: If a variable isn’t displayed in build() and doesn't affect how other widgets are displayed — it's not part of UI state and doesn't need setState().

Chapter 2: When setState IS Needed — Do It Precisely

There are cases where a variable truly affects UI, but that doesn’t mean you need to rebuild everything. Let’s look at smarter approaches.

❌ Typical case of excessive setState:

class ProductCard extends StatefulWidget {
  @override
  _ProductCardState createState() => _ProductCardState();
}

class _ProductCardState extends State<ProductCard> {
  bool _isFavorite = false;
  bool _isInCart = false;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Column(
        children: [
          Image.network('...'),
          Text('Product Name'),
          Row(
            children: [
              IconButton(
                icon: Icon(
                  _isFavorite ? Icons.favorite : Icons.favorite_border,
                  color: _isFavorite ? Colors.red : Colors.grey,
                ),
                onPressed: () {
                  setState(() {          // ❌ Rebuilds the ENTIRE Card!
                    _isFavorite = !_isFavorite;
                  });
                },
              ),
              IconButton(
                icon: Icon(
                  _isInCart ? Icons.shopping_cart : Icons.add_shopping_cart,
                  color: _isInCart ? Colors.green : Colors.grey,
                ),
                onPressed: () {
                  setState(() {          // ❌ Rebuilds the ENTIRE Card again!
                    _isInCart = !_isInCart;
                  });
                },
              ),
            ],
          ),
        ],
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Problem: We’re changing only the button color and icon, but rebuilding the entire product card with image, text, and all other elements!

✅ Solution 1: ValueNotifier for precise updates

class ProductCard extends StatelessWidget {
  final ValueNotifier<bool> _isFavorite = ValueNotifier<bool>(false);
  final ValueNotifier<bool> _isInCart = ValueNotifier<bool>(false);

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Column(
        children: [
          Image.network('...'),  // ❌ This part WON'T rebuild
          Text('Product Name'),   // ❌ This either
          Row(
            children: [
              // ✅ Only this icon will rebuild
              ValueListenableBuilder<bool>(
                valueListenable: _isFavorite,
                builder: (context, isFavorite, child) {
                  return IconButton(
                    icon: Icon(
                      isFavorite ? Icons.favorite : Icons.favorite_border,
                      color: isFavorite ? Colors.red : Colors.grey,
                    ),
                    onPressed: () {
                      _isFavorite.value = !isFavorite; // Just change value
                    },
                  );
                },
              ),
              // ✅ And this icon too
              ValueListenableBuilder<bool>(
                valueListenable: _isInCart,
                builder: (context, isInCart, child) {
                  return IconButton(
                    icon: Icon(
                      isInCart ? Icons.shopping_cart : Icons.add_shopping_cart,
                      color: isInCart ? Colors.green : Colors.grey,
                    ),
                    onPressed: () {
                      _isInCart.value = !isInCart;
                    },
                  );
                },
              ),
            ],
          ),
        ],
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

What changed:

  • Product image and text no longer rebuild on click
  • Only the specific icon updates
  • Code became more declarative

✅ Solution 2: Extract changing part into separate StatefulWidget

class FavoriteButton extends StatefulWidget {
  @override
  _FavoriteButtonState createState() => _FavoriteButtonState();
}

class _FavoriteButtonState extends State<FavoriteButton> {
  bool _isFavorite = false;

  @override
  Widget build(BuildContext context) {
    return IconButton(
      icon: Icon(
        _isFavorite ? Icons.favorite : Icons.favorite_border,
        color: _isFavorite ? Colors.red : Colors.grey,
      ),
      onPressed: () {
        setState(() {
          _isFavorite = !_isFavorite; // Only button rebuilds
        });
      },
    );
  }
}

// Use in main widget
class ProductCard extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Card(
      child: Column(
        children: [
          Image.network('...'),
          Text('Product Name'),
          Row(
            children: [
              FavoriteButton(),      // Isolated state
              CartButton(),          // Another isolated state
            ],
          ),
        ],
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Chapter 3: Checklist and Practical Rules

The 5-Second Rule
Before writing setState(), ask yourself:

“If I change this variable, will the user see visual changes within the next 5 seconds?”

If no (data goes to server, saves to DB, used for calculations) — you don’t need setState().

Simple Test for Your Code
Try temporarily commenting out setState():

onChanged: (value) {
  // setState(() {          // Commented out
    _someValue = value;    // Data still saves!
  // });
}
Enter fullscreen mode Exit fullscreen mode

If UI works correctly — setState was unnecessary.

Strategy Guide:

  1. Variable doesn’t affect UI → simple assignment
_apiToken = 'new_token';  // For sending requests
_userData = json;         // For processing
_cache = data;            // For storage
Enter fullscreen mode Exit fullscreen mode

2 Affects one small elementValueNotifier + ValueListenableBuilder
3 Affects several related widgets → extract to separate StatefulWidget
4 Affects entire screen/widgetsetState() (use rarely and deliberately)

Typical Scenarios Without setState:

// Logging and analytics
onPressed: () {
  _clickCounter++;        // Just a variable
  _logEvent('button_click');
}

// Timers for logic
Timer.periodic(Duration(seconds: 30), (timer) {
  _lastUpdate = DateTime.now();  // For internal logic
  _checkForUpdates();
});

// Data caching
Future<void> _loadData() async {
  final data = await api.fetchData();
  _cachedData = data;     // Save, but don't rebuild
  _processData(data);
}
Enter fullscreen mode Exit fullscreen mode

Conclusion: Ultimate Cheatsheet

When NOT to use setState:
• Data sent to server
• Saved to database/cache
• Used for logic/calculations
• For analytics/logging
• Temporary processing variables
When setState needed, but precisely:
• Button color/icon changes → ValueNotifier
• Text field updates → separate widget
• UI element state toggles → isolated state
When setState is justified:
• Large portion of screen changes
• Complex animation updates
• Entire list/table rebuilds
Enter fullscreen mode Exit fullscreen mode

Remember: Flutter is not React. You don’t need to update state on every change. Separate UI state from business logic, and your app will run smoothly even on low-end devices.

Every unnecessary setState() is a step from "works fine" to "slideshow mode." Be intentional about state management!
Buy Me a Coffee at ko-fi.com

Top comments (0)