Learn Flutter - Basic State Management
Episode 6 of 23

Learn Flutter - Basic State Management

This episode opens the world of state management: managing local state with setState, the lifting state up and prop drilling techniques, an introduction to the Provider pattern and InheritedWidget, and best practices for small-scale state before jumping to larger architectures.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

The larger an app grows, the more data must be shared between widgets. State management answers a simple but crucial question: where does data live and how do data changes trigger UI updates. Episode 6 is a controlled introduction to state management: starting with setState() for local state, then lifting state up and prop drilling to share state between widgets, an introduction to InheritedWidget and Provider as the official pattern, and best practices for small-scale apps before you move up to advanced architecture in episode 10.

Managing Local State with setState

State Within a Single Widget

setState() is the most basic mechanism. It fits when state is used only by one widget and its descendants:

Counter with setState
class _CounterState extends State<CounterWidget> {
  int _nilai = 0;
 
  void _tambah() {
    setState(() {
      _nilai++;
    });
  }
 
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Nilai: $_nilai'),
        ElevatedButton(
          onPressed: _tambah,
          child: const Text('Tambah'),
        ),
      ],
    );
  }
}

setState tells the framework that state has changed so build runs again. The principle: don't mutate state outside setState, because the UI won't be updated.

Lifting State Up and Prop Drilling

Moving State to an Ancestor

When two sibling widgets must share data, lift the state up to their common parent:

State lifted up to the parent
class ParentWidget extends StatefulWidget {
  const ParentWidget({super.key});
 
  @override
  State<ParentWidget> createState() => _ParentWidgetState();
}
 
class _ParentWidgetState extends State<ParentWidget> {
  bool _aktif = false;
 
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Switch(
          value: _aktif,
          onChanged: (nilai) => setState(() => _aktif = nilai),
        ),
        ChildWidget(aktif: _aktif),
      ],
    );
  }
}

_aktif lives in the parent, and ChildWidget receives it as a parameter. This technique is called lifting state up. Callbacks are passed down together with the data so the child can report changes to the parent.

Prop Drilling: The Cost of Lifting

As the widget tree gets deeper, passing data through parameters over and over is called prop drilling. At two or three levels deep it's still reasonable, but for larger apps this approach makes code noisy and hard to maintain. That's where the patterns in the next section come in.

Patterns: InheritedWidget and Provider

InheritedWidget

InheritedWidget is Flutter's mechanism for sharing data across an entire subtree without passing parameters one by one. Data is accessed through context.dependOnInheritedWidgetOfExactType.

Although you'll rarely write an InheritedWidget directly, understanding it matters because Provider is built on top of it:

The concept of accessing data through context
BuildContext context;
 
final data = context.dependOnInheritedWidgetOfExactType<DataProvider>();

context.dependOnInheritedWidgetOfExactType<T>() looks up the nearest InheritedWidget and subscribes this widget to its changes.

Provider as the Official Abstraction

Writing InheritedWidgets by hand is error-prone. The solution: the Provider package, which wraps InheritedWidget behind a simple API:

Add provider to the project
flutter pub add provider

Once installed, wrap the app with ChangeNotifierProvider:

Provide state with provider
ChangeNotifierProvider(
  create: (context) => CounterModel(),
  child: const MaterialApp(home: CounterScreen()),
)

ChangeNotifierProvider(create: ...) creates an instance of CounterModel and shares it across the entire tree below. CounterModel extends ChangeNotifier and calls notifyListeners() when its data changes.

Consume state with watch
class CounterModel extends ChangeNotifier {
  int _nilai = 0;
  int get nilai => _nilai;
 
  void tambah() {
    _nilai++;
    notifyListeners();
  }
}
 
class CounterScreen extends StatelessWidget {
  const CounterScreen({super.key});
 
  @override
  Widget build(BuildContext context) {
    final model = context.watch<CounterModel>();
    return Scaffold(
      body: Center(child: Text('Nilai: ${model.nilai}')),
      floatingActionButton: FloatingActionButton(
        onPressed: model.tambah,
        child: const Icon(Icons.add),
      ),
    );
  }
}

context.watch<CounterModel> makes the widget rebuild when the model changes. The key rule: use watch for reads that must trigger a rebuild, and read for actions that shouldn't.

Best Practices for Small-Scale State

When to Use What

For small to medium apps, these rules are enough:

  • State used by only one widget: use setState.
  • State shared by a few nearby widgets: lift state up.
  • State used across screens or deep subtrees: Provider.
  • Avoid storing data in state that never changes.
Verify installed dependencies
flutter pub deps

flutter pub deps shows the project's dependency tree — useful for checking which packages are already installed before adding another.

Consistency Matters More Than Sophistication

Pick one pattern and apply it consistently across the project. At a small scale, a mix of setState and Provider used in the right places is actually easier to maintain than forcing a single architecture for everything. Episode 10 will cover advanced patterns as the app grows large.

Conclusion

Key takeaways:

  • setState is enough for state used by a single widget.
  • Lifting state up: raise state to a common ancestor when several widgets share data.
  • Prop drilling becomes expensive in deep trees — time to switch patterns.
  • Provider is built on InheritedWidget and is the safe, official abstraction.
  • context.watch for rebuilds; context.read for actions without rebuild.
  • At small scale, pattern consistency matters more than sophistication.

In the next episode 7 we discuss theming and styling — ThemeData and Material Design theming, custom fonts, colors, and typography, dark mode support and adaptive UI, and styling widgets with decoration and responsive design. Your app starts looking professional.

Learn Flutter - Basic State Management | Learn Flutter