Working with InheritedWidget and InheritedNotifier in Flutter

Flutter is known for its powerful and flexible approach to state management. Among the many tools Flutter provides, InheritedWidget and InheritedNotifier are particularly useful for propagating data down the widget tree in an efficient and reactive manner. Understanding and utilizing these tools can greatly improve your application’s architecture and maintainability.

What are InheritedWidget and InheritedNotifier?

InheritedWidget

InheritedWidget is a fundamental class in Flutter that allows widgets to efficiently propagate information down the widget tree. It enables descendant widgets to access data stored in an ancestor widget without the need for explicitly passing it through constructors.

InheritedNotifier

InheritedNotifier is a specialized version of InheritedWidget that combines the capabilities of an InheritedWidget with a Listenable object (such as a ValueNotifier or ChangeNotifier). This allows widgets to not only access data but also react to changes in that data, triggering rebuilds of dependent widgets when the data changes.

Why Use InheritedWidget and InheritedNotifier?

  • Centralized Data: Provides a centralized place to store and manage data.
  • Efficient Propagation: Propagates data efficiently through the widget tree.
  • Reactivity: Allows widgets to react to changes in the data.
  • Reduced Boilerplate: Reduces the amount of boilerplate code required to pass data between widgets.
  • Improved Performance: Optimizes widget rebuilds by only rebuilding widgets that depend on the changed data.

How to Implement InheritedWidget

Step 1: Create an InheritedWidget

First, create a class that extends InheritedWidget. You’ll need to override the updateShouldNotify method, which determines whether dependent widgets should rebuild when the data changes.


import 'package:flutter/material.dart';

class DataInheritedWidget extends InheritedWidget {
  final String data;

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

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

  static DataInheritedWidget of(BuildContext context) {
    final DataInheritedWidget? result = context.dependOnInheritedWidgetOfExactType();
    assert(result != null, 'No DataInheritedWidget found in context');
    return result!;
  }
}

Explanation:

  • DataInheritedWidget extends InheritedWidget and holds a data property.
  • The constructor takes the data and a child.
  • updateShouldNotify checks if the data has changed and returns true if a rebuild is required.
  • The static of method provides a convenient way to access the InheritedWidget from descendant widgets using context.dependOnInheritedWidgetOfExactType.

Step 2: Use the InheritedWidget

Wrap a portion of your widget tree with the InheritedWidget. Any descendant widgets can then access the data.


import 'package:flutter/material.dart';

class MyScreen extends StatefulWidget {
  const MyScreen({Key? key}) : super(key: key);

  @override
  _MyScreenState createState() => _MyScreenState();
}

class _MyScreenState extends State<MyScreen> {
  String data = "Initial Data";

  void updateData(String newData) {
    setState(() {
      data = newData;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('InheritedWidget Example'),
      ),
      body: DataInheritedWidget(
        data: data,
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              DataDisplayWidget(),
              ElevatedButton(
                onPressed: () {
                  updateData("Updated Data");
                },
                child: const Text('Update Data'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class DataDisplayWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final inheritedWidgetData = DataInheritedWidget.of(context).data;
    return Text('Data: $inheritedWidgetData');
  }
}

In this example:

  • MyScreen is a stateful widget that holds the data and a button to update it.
  • DataInheritedWidget wraps the Center widget and provides the current value of data.
  • DataDisplayWidget is a stateless widget that accesses the data using DataInheritedWidget.of(context).data. When the data changes, DataDisplayWidget is rebuilt to reflect the new value.

How to Implement InheritedNotifier

Step 1: Create a ChangeNotifier

First, create a class that extends ChangeNotifier to hold the data that you want to propagate and notify listeners about.


import 'package:flutter/foundation.dart';

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

  String get data => _data;

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

Explanation:

  • DataNotifier extends ChangeNotifier and holds the _data property with a getter.
  • The updateData method updates the data and calls notifyListeners(), which rebuilds all widgets that depend on this ChangeNotifier.

Step 2: Create an InheritedNotifier

Create an InheritedWidget that uses the ChangeNotifier to provide data and notify listeners.


import 'package:flutter/material.dart';

class DataInheritedNotifier extends InheritedNotifier {
  const DataInheritedNotifier({
    Key? key,
    required DataNotifier dataNotifier,
    required Widget child,
  }) : super(key: key, notifier: dataNotifier, child: child);

  static DataNotifier of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType()!.notifier!;
  }
}

Explanation:

  • DataInheritedNotifier extends InheritedNotifier<DataNotifier> and holds a DataNotifier.
  • The constructor takes the DataNotifier and a child.
  • The static of method provides a convenient way to access the DataNotifier from descendant widgets.

Step 3: Use the InheritedNotifier

Wrap a portion of your widget tree with the InheritedNotifier. Any descendant widgets can then access the data and react to changes.


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

class MyScreen extends StatefulWidget {
  const MyScreen({Key? key}) : super(key: key);

  @override
  _MyScreenState createState() => _MyScreenState();
}

class _MyScreenState extends State<MyScreen> {
  final dataNotifier = DataNotifier();

  @override
  void dispose() {
    dataNotifier.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('InheritedNotifier Example'),
      ),
      body: DataInheritedNotifier(
        dataNotifier: dataNotifier,
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              DataDisplayWidget(),
              ElevatedButton(
                onPressed: () {
                  dataNotifier.updateData("Updated Data");
                },
                child: const Text('Update Data'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class DataDisplayWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final dataNotifier = DataInheritedNotifier.of(context);
    return Text('Data: ${dataNotifier.data}');
  }
}

In this example:

  • MyScreen is a stateful widget that holds a DataNotifier instance.
  • DataInheritedNotifier wraps the Center widget and provides the dataNotifier.
  • DataDisplayWidget is a stateless widget that accesses the data using DataInheritedNotifier.of(context).data. When the data changes via dataNotifier.updateData(), DataDisplayWidget is rebuilt to reflect the new value.

Comparing InheritedWidget and InheritedNotifier

  • InheritedWidget:
    • Requires manual implementation of updateShouldNotify to determine when to rebuild dependent widgets.
    • Useful for simple data propagation that doesn’t require frequent updates.
  • InheritedNotifier:
    • Automatically handles rebuilds using Listenable objects like ChangeNotifier or ValueNotifier.
    • Suitable for reactive data that changes frequently and requires immediate UI updates.

Conclusion

InheritedWidget and InheritedNotifier are essential tools for efficient state management in Flutter. While InheritedWidget provides a basic mechanism for propagating data down the widget tree, InheritedNotifier extends this with reactive capabilities, allowing widgets to react to data changes automatically. Understanding and using these tools can lead to more maintainable, performant, and reactive Flutter applications.