Inherited Widgets are easy, it is their documentation that sucks

January 01, 2023

As the world celebrates another trip of the earth around the sun, I found myself trying to figure out how to use InheritedWidgets.

And what I found is that the reputation that InheritedWidgets have for being unwieldy is more because their documentation is lacking than anything inherent in the InheritedWidgets themselves.

Hopefully this short tutorial will clarify them for future programmers, so they don't have to spend their New Year's digging into obscurity 🙂


The base problem we find ourselves solving when writing apps is:

  • Something changes here,
  • And we want something else to change there.

Let's solve this in a simple 35 line app that shows a counter button.

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: MyWidget(),
    );
  }
}

class MyWidget extends StatefulWidget {
  const MyWidget({super.key});

  
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  int c = 0;

  void increment() {
    setState(() {
      c++;
    });
  }

  
  Widget build(BuildContext context) {
    return TextButton(
      child: Text('$c'),
      onPressed: increment,
    );
  }
}

Here the base problem is solved by Flutter's setState:

  • Something changes here (c++),
  • And we want something else to change there ("our widget gets rerendered").

Now suppose the TextButton was in a different layer. This is common - as widgets grow bigger we break them into smaller ones. In a real app, the TextButton might be a deeply nested widget that needs to be passed the count.

One way is to, well, just pass the count as an argument to all the widgets on the way. This is a legit better solution in many cases: simple and explict. But not always: in a lot of cases, having everything be passed explicitly (a) obscures from the true intent of the code, and (b) makes refactoring harder.

Flutter's native solution for this is InheritedWidgets. Let's see how.

First, let us create an inherited widget that keeps track of the count.

class Count extends InheritedWidget {
  const Count({super.key, required super.child});

  final int c = 0;

  void increment() { /* TODO */ }

  static Count of(BuildContext context) =>
      context.dependOnInheritedWidgetOfExactType<Count>()!;

  
  bool updateShouldNotify(Count old) => c != old.c;
}

Then let us get hold of it in our nested widget that need to show the count.

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

  
  Widget build(BuildContext context) {
    final c = Count.of(context).c;
    return TextButton(
      child: Text('$c'),
      onPressed: Count.of(context).increment,
    );
  }
}

And finally, let us replace our original top level button with the inherited widget. All children of the inherited widget will have access to Count.of(context), including our nested widget where it is actually needed.

class _MyWidgetState extends State<MyWidget> {
  ...
  Widget build(BuildContext context) {
    return Count(child: NestedWidget());
  }
}

To be fair, the documentation is actually pretty clear until this point. Here is where I felt it drops the ball - it doesn't explain mutations (or maybe it explains but I didn't get them).

To put it differently, it doesn't explain how to implement the increment method where we left the TODO above. And this was a head scratcher for me, since the inherited widget is stateless.

The solution is quite simple though, the classic programming saw of adding another level of indirection: while the inherited widget itself is a stateless widget, we can wrap it in a stateful widget that stores the state, and rebuilds the inherited widget whenever the state changes.

Let's put that in action in our example. First, let us create a stateful widget that holds the count and a function to increment the count.

class CountContainer extends StatefulWidget {
  const CountContainer({super.key, required this.child});
  final Widget child;

  
  State<CountContainer> createState() => _CountContainerState();
}

class _CountContainerState extends State<CountContainer> {
  int c = 0;

  void increment() {
    setState(() {
      c++;
    });
  }

  
  Widget build(BuildContext context) {
    return Count(c: c, increment: increment, child: widget.child);
  }
}

The Count widget then gets all of these passed as parameters.

class Count extends InheritedWidget {
  const Count({
    super.key,
    required this.c,
    required this.increment,
    required super.child,
  });

  final int c;
  final void Function() increment;

  static Count of(BuildContext context) =>
      context.dependOnInheritedWidgetOfExactType<Count>()!;

  
  bool updateShouldNotify(Count old) => c != old.c;
}

Finally, instead of directly adding the Count, the top level widget adds the CountContainer.

class _MyWidgetState extends State<MyWidget> {
  ...
  Widget build(BuildContext context) {
    return CountContainer(child: NestedWidget());
  }
}

That is the general recipe - if the inherited widget needs to keep track of mutable state, wrap it in a stateful widget container.

However, in this particular case we already have a natural place to keep the state: the top level MyWidget itself! So the redundant container widget can be removed, and the inherited widget can be directly used as it is.

class _MyWidgetState extends State<MyWidget> {
  int c = 0;

  void increment() {
    setState(() {
      c++;
    });
  }

  
  Widget build(BuildContext context) {
    return Count(c: c, increment: increment, child: NestedWidget());
  }
}

Here is a runnable version of the full example: at 65 lines, it is only 30 lines more than what we'd started with, but it now includes this useful way of passing state without writing out the dependencies explicitly.

And here you can see a real example of an inherited widget in use in the Ente Photos app. No third party dependencies, and everything feels nice and solid. Happy 2023!