This episode levels up to medium-scale state management: choosing and using the Provider, Riverpod, Bloc, GetX, and MobX patterns, understanding reactive state flow and dependency injection, managing complex state with modularization, and the criteria for choosing an architecture based on app size.

setState and Provider are enough for small apps, but as features grow — auth, shopping carts, synchronization, notifications — state management must be organized with discipline. Episode 10 compares the patterns most widely used by the community and helps you choose with reasons, not by following the crowd.
We discuss Provider, Riverpod, Bloc, GetX, and MobX, understand reactive state flow and dependency injection, explore modularization techniques for complex state, and build a framework for choosing an architecture based on app size and team.
flutter pub add flutter_riverpodWith Riverpod, state is defined as a provider separate from widgets:
final counterProvider = StateProvider<int>((ref) => 0);
class CounterScreen extends ConsumerWidget {
const CounterScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Text('Nilai: $count');
}
}StateProvider stores a simple value, and ref.watch(counterProvider) makes the widget react to its changes. ConsumerWidget replaces StatelessWidget for widgets that consume providers.
All the patterns above follow the unidirectional data flow principle: state produces UI, interactions produce events, events change state, and the cycle repeats. The most common mistake is mutating state from anywhere without a clear path.
Provider isn't just for state — it's also for sharing objects like repositories or HTTP clients:
final postRepositoryProvider = Provider<PostRepository>((ref) {
return PostRepository(httpClient: dio);
});postRepositoryProvider builds PostRepository once and shares it across the whole app. This is dependency injection in its simplest form: widgets receive dependencies through providers instead of creating them themselves, so they can be easily overridden with mocks during testing.
A medium app needs state separated by domain:
AuthController — session and tokens.CartController — cart contents.OrderController — order status.Don't lump all state into one giant object. Each feature has its own provider or controller, and screens consume only what they need.
class CartController extends ChangeNotifier {
final List<Item> _items = [];
int get totalBarang => _items.length;
void tambah(Item item) {
_items.add(item);
notifyListeners();
}
}CartController manages a single responsibility. With this pattern, a change in one domain doesn't shake another — the key to maintainability.
The finer the provider granularity, the fewer widgets rebuild when state changes. The rule of thumb: listen at the smallest level. Don't watch a large state at the root if only one value is used — this is a cause of jank we'll discuss in episode 15.
Guidance for choosing a pattern:
flutter pub deps --style=compactflutter pub deps --style=compact shows the dependency tree as one line per package — practical for checking what's already in before adding a new pattern.
The best architecture is the one the whole team understands. Whatever you choose, document the state flow in the README, use one pattern consistently, and don't mix three patterns in one project without a reason. Episode 20 will map this onto a production-scale architecture.
Key takeaways:
In the next episode 11 we discuss navigation and app architecture — the difference between Navigator 1.0 and Navigator 2.0, named routes, nested routes, and deep linking, modular app architecture and feature modules, and code organization and separation of concerns. Your project structure starts moving to a real scale.