[{"data":1,"prerenderedAt":4},["ShallowReactive",2],{"post-content-flutter-state-management-3-rewrites":3},"\u003Cp>I've rewritten the state management layer of my Flutter apps three separate times. Not because I was chasing the shiny new thing. Because the thing I was using actually broke.\u003C\u002Fp>\n\n\u003Cp>Iconis, Prod Me, and Nexto all started with Provider. And all three ended up with a week-long detour where I ripped it out and replaced it with something else. The last time hurt enough that I sat down and figured out what I actually needed — not what Twitter said was cool.\u003C\u002Fp>\n\n\u003Cp>Here's what I learned.\u003C\u002Fp>\n\n\u003Ch2>The Three Stages of Flutter State Management Grief\u003C\u002Fh2>\n\n\u003Ch3>Stage 1: Provider (The Default)\u003C\u002Fh3>\n\n\u003Cp>When I built the first version of Iconis (an app icon studio), I used Provider because that's what the Flutter docs recommended. It worked fine for the first few weeks. Then I added authentication. Then cloud sync. Then a real-time preview that needed to react to 12 different streams simultaneously.\u003C\u002Fp>\n\n\u003Cp>Suddenly my widget tree looked like this:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-dart\">MultiProvider(\n  providers: [\n    ChangeNotifierProvider(create: (_) => AuthProvider()),\n    ChangeNotifierProvider(create: (_) => IconProvider()),\n    ChangeNotifierProvider(create: (_) => SyncProvider()),\n    ChangeNotifierProvider(create: (_) => ThemeProvider()),\n    ChangeNotifierProvider(create: (_) => ExportProvider()),\n    ChangeNotifierProxyProvider&lt;AuthProvider, SyncProvider&gt;(...),\n    \u002F\u002F it just kept going\n  ],\n  child: const MyApp(),\n)\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>The real problem wasn't the nesting. It was that every time \u003Ccode>SyncProvider\u003C\u002Fcode> changed, three unrelated widgets rebuilt. I spent more time wrapping things in \u003Ccode>Consumer\u003C\u002Fcode> and \u003Ccode>Selector\u003C\u002Fcode> than I did building actual features.\u003C\u002Fp>\n\n\u003Ch3>Stage 2: BLoC (The Over-Engineer)\u003C\u002Fh3>\n\n\u003Cp>For Prod Me (my persistent reminder app), I thought I'd learned my lesson. I went all-in on BLoC. Separate Event classes, State classes, Bloc classes. It was clean. It was testable. It also required 47 files to show a simple reminder list.\u003C\u002Fp>\n\n\u003Cp>Here's what shipping a tiny feature looked like:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-dart\">\u002F\u002F 1. Define event\nabstract class ReminderEvent {}\nclass AddReminderEvent extends ReminderEvent {\n  final Reminder reminder;\n  AddReminderEvent(this.reminder);\n}\n\n\u002F\u002F 2. Define state\nclass ReminderState {\n  final List&lt;Reminder&gt; reminders;\n  final bool isLoading;\n  ReminderState({this.reminders = const [], this.isLoading = false});\n}\n\n\u002F\u002F 3. Create bloc\nclass ReminderBloc extends Bloc&lt;ReminderEvent, ReminderState&gt; {\n  ReminderBloc() : super(ReminderState()) {\n    on&lt;AddReminderEvent&gt;((event, emit) {\n      emit(state.copyWith(reminders: [...state.reminders, event.reminder]));\n    });\n  }\n}\n\n\u002F\u002F 4. Provide bloc\nBlocProvider(create: (_) => ReminderBloc())\n\n\u002F\u002F 5. Use in widget\nBlocBuilder&lt;ReminderBloc, ReminderState&gt;(\n  builder: (context, state) {\n    return ListView(...);\n  },\n)\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>Five files for adding an item to a list. The architecture was clean — I'll give it that. But I was spending 60% of my time writing boilerplate and 30% debugging \"I forgot to add the event to the right Bloc\" errors. Something had to give.\u003C\u002Fp>\n\n\u003Ch3>Stage 3: setState (The Overcorrection)\u003C\u002Fh3>\n\n\u003Cp>For Nexto (my todo app), I went the opposite direction. No state management library at all. Just good old \u003Ccode>setState\u003C\u002Fcode> with a service layer. And you know what? It worked shockingly well for the first 500 lines.\u003C\u002Fp>\n\n\u003Cp>Then I added drag-and-drop reordering. And filtering. And search. And suddenly I was passing callback functions through five layers of widgets and praying I hadn't created a memory leak.\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-typescript\">\u002F\u002F My \"service layer\" started looking like this:\nclass TodoService {\n  final List&lt;VoidCallback&gt; _listeners = [];\n\n  void addListener(VoidCallback cb) => _listeners.add(cb);\n  void removeListener(VoidCallback cb) => _listeners.remove(cb);\n  void _notify() => _listeners.forEach((cb) => cb());\n\n  \u002F\u002F I basically rebuilt ChangeNotifier by hand\n  \u002F\u002F and did a worse job of it\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>That's when I finally admitted I was the problem. I was ping-ponging between architecture extremes instead of finding the middle ground.\u003C\u002Fp>\n\n\u003Ch2>What I Actually Needed\u003C\u002Fh2>\n\n\u003Cp>After three apps of painful rewrites, I made a list of what I actually wanted from a state management solution:\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>No boilerplate\u003C\u002Fstrong> — I don't want to write 5 files to show a list of items\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Fine-grained rebuilds\u003C\u002Fstrong> — changing one field shouldn't rebuild the whole page\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Testable without ceremony\u003C\u002Fstrong> — I should be able to test logic without a widget test harness\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Works with async\u003C\u002Fstrong> — API calls, local DB queries, file operations — the common stuff\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Not a code generator\u003C\u002Fstrong> — I don't want to run a command every time I add a feature\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>That's when I tried Riverpod. And honestly? I should have started here.\u003C\u002Fp>\n\n\u003Ch2>How I Structure Flutter Apps Now\u003C\u002Fh2>\n\n\u003Cp>Here's the pattern I've settled on after shipping three apps with Riverpod. It's not revolutionary, but it's practical and it works.\u003C\u002Fp>\n\n\u003Ch3>The One-File Provider Pattern\u003C\u002Fh3>\n\n\u003Cp>Instead of spreading logic across events, states, blocs, and providers, I keep related logic in one file:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-dart\">\u002F\u002F providers\u002Freminder_provider.dart\n\nfinal reminderRepositoryProvider = Provider&lt;ReminderRepository&gt;((ref) {\n  return ReminderRepository();\n});\n\nfinal remindersProvider = AsyncNotifierProvider&lt;RemindersNotifier, List&lt;Reminder&gt;&gt;(() {\n  return RemindersNotifier();\n});\n\nclass RemindersNotifier extends AsyncNotifier&lt;List&lt;Reminder&gt;&gt; {\n  @override\n  Future&lt;List&lt;Reminder&gt;&gt; build() async {\n    final repo = ref.read(reminderRepositoryProvider);\n    return repo.fetchAll();\n  }\n\n  Future&lt;void&gt; addReminder(Reminder reminder) async {\n    final repo = ref.read(reminderRepositoryProvider);\n    await repo.save(reminder);\n    ref.invalidateSelf(); \u002F\u002F re-fetch the list\n  }\n\n  Future&lt;void&gt; toggleComplete(String id) async {\n    \u002F\u002F Optimistic update\n    state = AsyncData(state.value!.map((r) {\n      return r.id == id ? r.copyWith(isCompleted: !r.isCompleted) : r;\n    }).toList());\n\n    final repo = ref.read(reminderRepositoryProvider);\n    await repo.toggle(id);\n  }\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>That's it. One file. One provider for the repository, one notifier for the state. \u003Ccode>build()\u003C\u002Fcode> fetches initial data, methods update it. \u003Ccode>ref.invalidateSelf()\u003C\u002Fcode> tells Riverpod \"this data is stale, re-fetch it.\"\u003C\u002Fp>\n\n\u003Ch3>Where I Keep State\u003C\u002Fh3>\n\n\u003Cp>I split my providers into three categories:\u003C\u002Fp>\n\n\u003Col>\n  \u003Cli>\u003Cstrong>Data providers\u003C\u002Fstrong> — API clients, database instances, shared preferences. Created once, injected everywhere.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>State notifiers\u003C\u002Fstrong> — AsyncNotifier or Notifier classes that hold mutable UI state. One per feature screen.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Derived providers\u003C\u002Fstrong> — Computed values. Filtered lists, calculated totals, combined data from multiple sources.\u003C\u002Fli>\n\u003C\u002Fol>\n\n\u003Cpre>\u003Ccode class=\"language-dart\">\u002F\u002F Derived provider example: filtered reminders\nfinal filteredRemindersProvider = Provider&lt;List&lt;Reminder&gt;&gt;((ref) {\n  final reminders = ref.watch(remindersProvider);\n  final filter = ref.watch(reminderFilterProvider);\n\n  if (filter == 'active') {\n    return reminders.value!.where((r) => !r.isCompleted).toList();\n  }\n  if (filter == 'completed') {\n    return reminders.value!.where((r) => r.isCompleted).toList();\n  }\n  return reminders.value ?? [];\n});\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>Derived providers auto-recompute when their dependencies change. No manual notification. No \u003Ccode>addListener\u003C\u002Fcode> \u002F \u003Ccode>removeListener\u003C\u002Fcode>. It just works.\u003C\u002Fp>\n\n\u003Ch3>Testing Without Pain\u003C\u002Fh3>\n\n\u003Cp>This is where Riverpod really shines. Testing a notifier is dead simple:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-dart\">void main() {\n  test('adds a reminder to the list', () async {\n    final container = ProviderContainer();\n    final notifier = container.read(remindersProvider.notifier);\n\n    await notifier.addReminder(\n      Reminder(title: 'Test reminder', priority: Priority.high),\n    );\n\n    final state = container.read(remindersProvider);\n    expect(state.value!.length, 1);\n    expect(state.value!.first.title, 'Test reminder');\n  });\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>No \u003Ccode>WidgetTester\u003C\u002Fcode>. No mocking \u003Ccode>BuildContext\u003C\u002Fcode>. Just a \u003Ccode>ProviderContainer\u003C\u002Fcode> and a test. I can run these in milliseconds instead of seconds.\u003C\u002Fp>\n\n\u003Ch2>When You Shouldn't Use Riverpod\u003C\u002Fh2>\n\n\u003Cp>I'm not here to sell you on Riverpod. It's not the right choice for every project:\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>Prototypes\u003C\u002Fstrong> — If you're building something that'll live for a weekend, use setState. Don't overthink it.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Simple apps\u003C\u002Fstrong> — A flashlight app doesn't need Riverpod. A counter app doesn't need Riverpod. Use what's simple.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Your team already knows BLoC\u003C\u002Fstrong> — The best state management is the one your team can ship with. Don't rewrite for the sake of rewriting (I say this as someone who did exactly that three times).\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Ch2>The Bottom Line\u003C\u002Fh2>\n\n\u003Cp>After rewriting state management in three production Flutter apps, I landed on Riverpod because it's the least opinionated about everything except what matters: your data flows one way, rebuilds are fine-grained, and testing is trivial.\u003C\u002Fp>\n\n\u003Cp>Is it perfect? No. The autodispose behavior took me a week to wrap my head around. And the modifier syntax (\u003Ccode>.family\u003C\u002Fcode>, \u003Ccode>.autoDispose\u003C\u002Fcode>) can get confusing. But for 90% of what I build, it's the right tool.\u003C\u002Fp>\n\n\u003Cp>If you're just starting a Flutter project and wondering what to reach for, skip Provider and try Riverpod. You'll save yourself a rewrite.\u003C\u002Fp>\n\n\u003Chr \u002F>\n\n\u003Cp>I built Iconis, Prod Me, and Nexto with Flutter. If you're working on a Flutter project too, check out \u003Ca href=\"https:\u002F\u002Fdevspera.com\u002Ficonis\u002F\">Iconis\u003C\u002Fa> for app icon design and \u003Ca href=\"https:\u002F\u002Fdevspera.com\u002Fsnippetark\u002F\">Snippet Ark\u003C\u002Fa> for keeping your code snippets organized across projects. What's your Flutter state management horror story? I'd love to hear it.\u003C\u002Fp>\n",1787133717910]