Flutter for Beginners: Simple State Management with InheritedWidget or Provider Concepts

Capítulo 9

Estimated reading time: 7 minutes

+ Exercise

Why Shared State Matters

In real apps, multiple widgets often need to read and update the same piece of data. Examples include: a “dark mode” setting that affects the whole app, a favorites list used on a list screen and a detail screen, or a logged-in user profile shown in several places.

If each screen keeps its own copy of the data, you quickly get problems: one screen updates, another screen doesn’t reflect the change; you end up passing values through many constructors; and it becomes hard to know where the “real” state lives.

Shared state solves this by storing the data in one place and letting any widget below it in the widget tree read it and react to changes.

Common beginner pain: “prop drilling”

When you pass state down through many widget constructors just so a deep child can use it, you create tight coupling and lots of boilerplate. This is sometimes called prop drilling. Shared state patterns reduce this by letting widgets access state from the context.

Two Simple Approaches: InheritedWidget and Provider-Style

Flutter’s core mechanism for sharing data down the tree is InheritedWidget. Many popular patterns (including Provider) build on this idea: put a state holder high in the tree, then read it anywhere below using BuildContext.

Continue in our app.
  • Listen to the audio with the screen off.
  • Earn a certificate upon completion.
  • Over 5000 courses for you to explore!
Or continue reading below...
Download App

Download the app

  • InheritedWidget: built-in, no dependencies, great for learning the underlying concept.
  • Provider-style: typically uses an external package, but conceptually it’s “an InheritedWidget + a notifier + helper methods.” You can implement a lightweight version yourself to understand how it works.

In this chapter, you’ll implement a small “Provider-style” pattern using InheritedNotifier (a convenient variant of InheritedWidget that listens to a Listenable such as ChangeNotifier).

When to Lift State Up vs Introduce Shared State

Lift state up when:

  • The state is only needed by a parent and its direct children (a small subtree).
  • The state changes are local to one screen or one feature area.
  • You can pass callbacks and values down 1–2 levels without pain.

Introduce shared state when:

  • Multiple screens need the same state (e.g., favorites across a list screen and a details screen).
  • You’re passing the same data through many widget constructors just to reach deep widgets.
  • You need a single source of truth for app-wide settings (theme mode, locale, authentication).

Rule of thumb

Start by lifting state up. If you notice repeated parameter passing, duplicated state, or cross-screen synchronization issues, move that state into a shared container placed higher in the widget tree.

Implementation Walkthrough: Shared Favorites Across Multiple Screens

You’ll build a minimal shared state system for a favorites list. The goal: a user can favorite items on a list screen, and the favorites screen updates automatically. A details screen can also toggle favorite state.

Step 1: Create the app state (ChangeNotifier)

This class holds the shared data and notifies listeners when it changes.

import 'package:flutter/foundation.dart';

class AppState extends ChangeNotifier {
  final Set<int> _favoriteIds = {};

  bool isFavorite(int id) => _favoriteIds.contains(id);

  List<int> get favorites => _favoriteIds.toList()..sort();

  void toggleFavorite(int id) {
    if (_favoriteIds.contains(id)) {
      _favoriteIds.remove(id);
    } else {
      _favoriteIds.add(id);
    }
    notifyListeners();
  }
}

notifyListeners() is the key: any widget that is “listening” will rebuild when favorites change.

Step 2: Create a lightweight provider using InheritedNotifier

This widget exposes AppState to the subtree via BuildContext. Because it extends InheritedNotifier, it automatically rebuilds dependents when the notifier changes.

import 'package:flutter/widgets.dart';
import 'app_state.dart';

class AppStateScope extends InheritedNotifier<AppState> {
  const AppStateScope({
    super.key,
    required AppState notifier,
    required Widget child,
  }) : super(notifier: notifier, child: child);

  static AppState of(BuildContext context) {
    final scope = context.dependOnInheritedWidgetOfExactType<AppStateScope>();
    assert(scope != null, 'No AppStateScope found in context');
    return scope!.notifier!;
  }
}

Important concept: dependOnInheritedWidgetOfExactType registers this widget as a dependent. When the notifier calls notifyListeners(), Flutter rebuilds dependents.

Step 3: Place the scope high enough in the widget tree

Wrap your app (or at least the part that needs shared state) with AppStateScope.

import 'package:flutter/material.dart';
import 'app_state.dart';
import 'app_state_scope.dart';
import 'screens/items_screen.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  MyApp({super.key});

  final AppState _state = AppState();

  @override
  Widget build(BuildContext context) {
    return AppStateScope(
      notifier: _state,
      child: MaterialApp(
        home: const ItemsScreen(),
      ),
    );
  }
}

Placing it above MaterialApp is common when many screens need it. If only a small section needs it, place it closer to that section to limit rebuild scope.

Step 4: Build an items list screen that reads and updates shared state

This screen shows items and lets the user toggle favorites. It reads state via AppStateScope.of(context).

import 'package:flutter/material.dart';
import '../app_state_scope.dart';
import 'favorites_screen.dart';
import 'item_details_screen.dart';

class ItemsScreen extends StatelessWidget {
  const ItemsScreen({super.key});

  @override
  Widget build(BuildContext context) {
    final appState = AppStateScope.of(context);
    final items = List.generate(20, (i) => i + 1);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Items'),
        actions: [
          IconButton(
            icon: const Icon(Icons.favorite),
            onPressed: () {
              Navigator.of(context).push(
                MaterialPageRoute(builder: (_) => const FavoritesScreen()),
              );
            },
          ),
        ],
      ),
      body: ListView.builder(
        itemCount: items.length,
        itemBuilder: (context, index) {
          final id = items[index];
          final isFav = appState.isFavorite(id);

          return ListTile(
            title: Text('Item $id'),
            trailing: IconButton(
              icon: Icon(isFav ? Icons.favorite : Icons.favorite_border),
              onPressed: () => appState.toggleFavorite(id),
            ),
            onTap: () {
              Navigator.of(context).push(
                MaterialPageRoute(
                  builder: (_) => ItemDetailsScreen(itemId: id),
                ),
              );
            },
          );
        },
      ),
    );
  }
}

What to notice: there is no local favorites state here. The list screen is simply a view over shared state.

Step 5: Create a favorites screen that stays in sync automatically

This screen reads the same shared state. When favorites change anywhere, this screen rebuilds because it depends on AppStateScope.

import 'package:flutter/material.dart';
import '../app_state_scope.dart';
import 'item_details_screen.dart';

class FavoritesScreen extends StatelessWidget {
  const FavoritesScreen({super.key});

  @override
  Widget build(BuildContext context) {
    final appState = AppStateScope.of(context);
    final favorites = appState.favorites;

    return Scaffold(
      appBar: AppBar(title: const Text('Favorites')),
      body: favorites.isEmpty
          ? const Center(child: Text('No favorites yet'))
          : ListView.builder(
              itemCount: favorites.length,
              itemBuilder: (context, index) {
                final id = favorites[index];
                return ListTile(
                  title: Text('Item $id'),
                  trailing: IconButton(
                    icon: const Icon(Icons.remove_circle_outline),
                    onPressed: () => appState.toggleFavorite(id),
                  ),
                  onTap: () {
                    Navigator.of(context).push(
                      MaterialPageRoute(
                        builder: (_) => ItemDetailsScreen(itemId: id),
                      ),
                    );
                  },
                );
              },
            ),
    );
  }
}

Step 6: Add a details screen that also uses the shared state

This demonstrates cross-screen consistency: toggling favorite here updates both the list and favorites screens.

import 'package:flutter/material.dart';
import '../app_state_scope.dart';

class ItemDetailsScreen extends StatelessWidget {
  final int itemId;
  const ItemDetailsScreen({super.key, required this.itemId});

  @override
  Widget build(BuildContext context) {
    final appState = AppStateScope.of(context);
    final isFav = appState.isFavorite(itemId);

    return Scaffold(
      appBar: AppBar(title: Text('Item $itemId')),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text('Details for item $itemId'),
            const SizedBox(height: 16),
            FilledButton.icon(
              onPressed: () => appState.toggleFavorite(itemId),
              icon: Icon(isFav ? Icons.favorite : Icons.favorite_border),
              label: Text(isFav ? 'Unfavorite' : 'Favorite'),
            ),
          ],
        ),
      ),
    );
  }
}

Understanding Rebuilds and Context Usage

Why does the UI update automatically?

Because AppStateScope.of(context) uses dependOnInheritedWidgetOfExactType, the calling widget becomes a dependent. When AppState calls notifyListeners(), InheritedNotifier marks dependents to rebuild.

Common beginner mistake: reading state in the wrong place

  • Don’t call AppStateScope.of(context) in a place where the context is above the scope (for example, in a widget that is created before the scope is inserted).
  • Do ensure the widget is below AppStateScope in the widget tree.

Reducing unnecessary rebuilds

In the examples above, the whole screen rebuilds when favorites change. That’s fine for small apps. If you want to rebuild only a small part (like a single list tile), you can move the AppStateScope.of(context) call deeper so fewer widgets depend on it.

// Example idea: read appState inside itemBuilder so only tiles depend on it
itemBuilder: (context, index) {
  final appState = AppStateScope.of(context);
  ...
}

Optional Variation: Sharing a Settings Toggle

The same pattern works for settings like “show only favorites” or “enable notifications.” You’d store a boolean in AppState, expose a getter, and call notifyListeners() when toggled. Any screen that reads the value will rebuild and reflect the new setting.

class AppState extends ChangeNotifier {
  bool _showOnlyFavorites = false;
  bool get showOnlyFavorites => _showOnlyFavorites;

  void toggleShowOnlyFavorites() {
    _showOnlyFavorites = !_showOnlyFavorites;
    notifyListeners();
  }
}

This is the same shared-state idea: one source of truth, multiple consumers, automatic UI updates.

Now answer the exercise about the content:

In a lightweight Provider-style setup using InheritedNotifier and ChangeNotifier, what causes widgets that read shared state to rebuild automatically when the favorites list changes?

You are right! Congratulations, now go to the next page

You missed! Try again.

Calling dependOnInheritedWidgetOfExactType makes a widget a dependent of the scope. When the ChangeNotifier calls notifyListeners(), the InheritedNotifier updates and dependents rebuild.

Next chapter

Flutter for Beginners: Lists, Scrolling, and Data Presentation

Arrow Right Icon
Free Ebook cover Flutter for Beginners: Build Your First Cross-Platform Apps from Scratch
75%

Flutter for Beginners: Build Your First Cross-Platform Apps from Scratch

New course

12 pages

Download the app to earn free Certification and listen to the courses in the background, even with the screen off.