DEV Community

Cover image for Your Flutter App is Dropping Frames: The True Cost of Bad State Management
Prajapati Paresh
Prajapati Paresh

Posted on • Originally published at smarttechdevs.in

Your Flutter App is Dropping Frames: The True Cost of Bad State Management

The "SetState" Spaghetti Monster

Every Flutter developer starts the same way: you build a beautiful UI, realize you need a button to update a text field somewhere else on the screen, and you wrap your entire screen in a StatefulWidget so you can call setState(). It works perfectly on your high-end development machine.

Six months later, your app is in production on mid-range Android devices, and your users are complaining that the app stutters every time they scroll or tap a button. Why? Because you are rebuilding the entire widget tree for a single localized change.

Global State is a Trap

The next mistake developers make is swinging completely in the opposite direction. They discover Provider or GetX and decide to put absolutely everything into global state. Now, instead of localized stuttering, you have an unmaintainable mess where your UI components are tightly coupled to massive, bloated controller classes.

Architecting with Riverpod (The Right Way)

To achieve a consistent 60FPS (or 120FPS) in a production Flutter application, you must isolate your rebuilds. We strictly use Riverpod, but we don't just use it blindly; we architect our state with razor-sharp precision.

The golden rule of our mobile architecture is Widget-Level Watching.


// Bad Architecture: Rebuilds the whole screen when the balance changes
class DashboardScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final balance = ref.watch(walletProvider).balance;
    return Column(
      children: [
        ComplexStaticGraphWidget(), // This gets needlessly rebuilt!
        Text('Balance: $balance'),
      ],
    );
  }
}

Instead, we extract the dependency into the smallest possible leaf widget:


// Good Architecture: The graph is completely safe from rebuilds
class DashboardScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        ComplexStaticGraphWidget(), // Never rebuilds
        BalanceTextWidget(), // Extracts the state logic into its own component
      ],
    );
  }
}

class BalanceTextWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // Only this tiny text widget rebuilds when the state changes
    final balance = ref.watch(walletProvider).balance;
    return Text('Balance: $balance');
  }
}

Conclusion

A beautiful UI is useless if it feels sluggish. By strictly isolating your state using modern tools like Riverpod and adopting a leaf-widget consumption strategy, you guarantee that your application scales beautifully without taxing the device's CPU.

Top comments (0)