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.

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.
setState() is the most basic mechanism. It fits when state is used only by one widget and its descendants:
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.
When two sibling widgets must share data, lift the state up to their common 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.
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.
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:
BuildContext context;
final data = context.dependOnInheritedWidgetOfExactType<DataProvider>();context.dependOnInheritedWidgetOfExactType<T>() looks up the nearest InheritedWidget and subscribes this widget to its changes.
Writing InheritedWidgets by hand is error-prone. The solution: the Provider package, which wraps InheritedWidget behind a simple API:
flutter pub add providerOnce installed, wrap the app with ChangeNotifierProvider:
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.
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.
For small to medium apps, these rules are enough:
setState.flutter pub depsflutter pub deps shows the project's dependency tree — useful for checking which packages are already installed before adding another.
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.
Key takeaways:
setState is enough for state used by a single widget.context.watch for rebuilds; context.read for actions without rebuild.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.