Working with InheritedWidget and InheritedNotifier to Efficiently Share Data Down the Widget Tree Without Prop Drilling in Flutter

In Flutter, managing and sharing data efficiently across the widget tree is crucial for building scalable and maintainable applications. Prop drilling—passing data through multiple layers of widgets that don’t need the data—can lead to verbose and hard-to-maintain code. InheritedWidget and InheritedNotifier provide elegant solutions to this problem, allowing widgets to access data from ancestor widgets without direct prop passing. This article explores how to use these tools effectively to share data in Flutter applications.

Understanding InheritedWidget

InheritedWidget is a base class in Flutter for widgets that efficiently propagate information down the widget tree. When the data in an InheritedWidget changes, only the widgets that depend on that data are rebuilt, optimizing performance.

Key Concepts:

  • Data Propagation: Shares data implicitly down the tree.
  • Efficient Rebuilds: Only rebuilds widgets that depend on the inherited data.
  • Read-Only Data Access: Typically used for read-only data or data managed externally.

Implementing InheritedWidget

To create an InheritedWidget, you need to:

  1. Create a class that extends InheritedWidget.
  2. Override the updateShouldNotify method to determine if the widget should notify its dependents of changes.
  3. Expose the data you want to share.

import 'package:flutter/material.dart';

class DataModel {
  final String data;

  DataModel({required this.data});
}

class MyInheritedWidget extends InheritedWidget {
  final DataModel dataModel;

  const MyInheritedWidget({
    Key? key,
    required this.dataModel,
    required Widget child,
  }) : super(key: key, child: child);

  @override
  bool updateShouldNotify(MyInheritedWidget oldWidget) {
    return oldWidget.dataModel.data != dataModel.data;
  }

  static MyInheritedWidget? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType();
  }
}

Usage in a widget tree:


class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyInheritedWidget(
        dataModel: DataModel(data: "Initial Data"),
        child: MyHomePage(),
      ),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('InheritedWidget Example')),
      body: Center(
        child: MyDataDisplayWidget(),
      ),
    );
  }
}

class MyDataDisplayWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final dataModel = MyInheritedWidget.of(context)?.dataModel;

    return Text('Data: ${dataModel?.data}');
  }
}

In this example, MyDataDisplayWidget can access the data from MyInheritedWidget without needing it passed directly as a property.

Understanding InheritedNotifier

InheritedNotifier combines the benefits of InheritedWidget with Listenable (e.g., ValueNotifier or ChangeNotifier), making it suitable for scenarios where you need to share mutable data and notify dependent widgets of changes. By using an InheritedNotifier, you can automatically trigger widget rebuilds when the value held by the Listenable changes.

Key Concepts:

  • Mutable Data Sharing: Allows sharing of mutable data down the tree.
  • Automatic Widget Rebuilds: Widgets are rebuilt automatically when the Listenable value changes.
  • Integration with Listenable: Uses ValueNotifier or ChangeNotifier to manage state.

Implementing InheritedNotifier with ValueNotifier

ValueNotifier is a simple Listenable that holds a single value. When the value changes, it notifies its listeners.


import 'package:flutter/material.dart';

class CounterNotifier extends ValueNotifier<int> {
  CounterNotifier(int value) : super(value);

  void increment() {
    value++;
  }
}

class MyInheritedNotifier extends InheritedNotifier<CounterNotifier> {
  const MyInheritedNotifier({
    Key? key,
    required CounterNotifier notifier,
    required Widget child,
  }) : super(key: key, notifier: notifier, child: child);

  static CounterNotifier? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType()?.notifier;
  }
}

Usage in a widget tree:


class MyApp extends StatelessWidget {
  final CounterNotifier _counterNotifier = CounterNotifier(0);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyInheritedNotifier(
        notifier: _counterNotifier,
        child: MyHomePage(),
      ),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('InheritedNotifier Example')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('You have pushed the button this many times:'),
            CounterDisplayWidget(),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          MyInheritedNotifier.of(context)?.increment();
        },
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

class CounterDisplayWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final counterNotifier = MyInheritedNotifier.of(context);

    return ValueListenableBuilder<int>(
      valueListenable: counterNotifier!,
      builder: (context, value, child) {
        return Text(
          '$value',
          style: Theme.of(context).textTheme.headline4,
        );
      },
    );
  }
}

In this example:

  • CounterNotifier extends ValueNotifier<int> and manages the counter state.
  • MyInheritedNotifier shares the CounterNotifier instance down the tree.
  • CounterDisplayWidget uses ValueListenableBuilder to listen for changes and rebuild when the counter value updates.

Implementing InheritedNotifier with ChangeNotifier

ChangeNotifier is another Listenable often used for more complex state management scenarios, where multiple properties can change.


import 'package:flutter/material.dart';

class DataModel extends ChangeNotifier {
  String _data = "Initial Data";

  String get data => _data;

  void updateData(String newData) {
    _data = newData;
    notifyListeners();
  }
}

class MyInheritedNotifier extends InheritedNotifier<DataModel> {
  const MyInheritedNotifier({
    Key? key,
    required DataModel notifier,
    required Widget child,
  }) : super(key: key, notifier: notifier, child: child);

  static DataModel? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType()?.notifier;
  }
}

Usage in a widget tree:


class MyApp extends StatelessWidget {
  final DataModel _dataModel = DataModel();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyInheritedNotifier(
        notifier: _dataModel,
        child: MyHomePage(),
      ),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('InheritedNotifier Example')),
      body: Center(
        child: DataDisplayWidget(),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          final dataModel = MyInheritedNotifier.of(context);
          dataModel?.updateData("Updated Data");
        },
        tooltip: 'Update Data',
        child: const Icon(Icons.update),
      ),
    );
  }
}

class DataDisplayWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final dataModel = MyInheritedNotifier.of(context);

    return AnimatedBuilder(
      animation: dataModel!,
      builder: (context, child) {
        return Text(
          'Data: ${dataModel.data}',
          style: Theme.of(context).textTheme.headline4,
        );
      },
    );
  }
}

In this example:

  • DataModel extends ChangeNotifier and manages the data.
  • MyInheritedNotifier shares the DataModel instance.
  • DataDisplayWidget uses AnimatedBuilder to listen for changes and rebuild when the data updates.

Best Practices

  • Use Sparingly: While InheritedWidget and InheritedNotifier are powerful, avoid overusing them. For very local state, consider using StatefulWidget or other state management solutions.
  • Immutable Data: With InheritedWidget, prefer using immutable data to ensure predictable behavior.
  • Efficient Notifications: Ensure that updateShouldNotify in InheritedWidget and notifications in ChangeNotifier are efficient to prevent unnecessary rebuilds.
  • Clear Scoping: Limit the scope of your InheritedWidget or InheritedNotifier to where the data is actually needed.

Conclusion

InheritedWidget and InheritedNotifier are valuable tools for managing and sharing data efficiently down the Flutter widget tree without resorting to prop drilling. Whether you need to share read-only data or mutable state, these widgets can help you write cleaner, more maintainable code. By understanding their strengths and applying them judiciously, you can build Flutter applications with better architecture and performance.