marko devcic

Software Engineer
  • github:
    deva666
  • email:
    madevcic {at} gmail.com
Flutter inherited widget

Flutter inherited widget

Posted on 10/13/2021

When I first heard about Flutter's inherited widget I had no idea why would I ever want to use one. Flutter docs only said it is used to propagate information down the widget tree. Hmm, propagate information down the widget tree? Don't we have listeners that can do that? Why do we need a widget for it? The more I used Flutter it became more obvious why this Widget exists and I have a sample problem that perfectly demonstrates how useful it can be.

Let's say you have a list view of expensive to build widgets, and they are not visible when a user first opens an app. But the widget/page that hosts this list view is built when the app opens (for example we are using a Bottom Navigation Bar). Here's some code that builds our Home Widget.


@override  
Widget build(BuildContext context) {  
  return Scaffold(  
  appBar: AppBar(title: Text(widget.title),),  
    body: AnimatedBuilder(animation: animationController, builder: (ctx, child) {  
      return Stack(children: childViews.map((v) {  
          int navIndex = childViews.indexOf(v);  
          bool isCurrent = navIndex == childIndex;  
          return TickerMode(enabled: isCurrent, // disable animations on all widgets except the topmost one
              child: Offstage(offstage: !isCurrent, // don't paint widgets except the the topmost one 
                                child: Opacity(  
                                    opacity: animationController.isAnimating ? fadeInAnimation.value : 1,  
                                    child: Transform.translate(  
                                          offset: animationController.isAnimating ? slideInAnimation.value : Offset.zero,  
                                        child: v)))));  
                  }).toList());  
            }),  
    bottomNavigationBar: BottomNavigationBar(onTap: changeChild,  
      currentIndex: childIndex,  
      items: const [  
        BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),  
        BottomNavigationBarItem(icon: Icon(Icons.bar_chart), label: 'List'),  
      ],    
  );  
}

And here's the expensive list item.


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

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

class _ExpensiveListItemState extends State {  

    @override  
    Widget build(BuildContext context) {  
        return FutureBuilder(  // Simulate something expensive
                future: Future.delayed(const Duration(seconds: 3)),  
        builder: (c, snapshot) {  
            if (snapshot.connectionState == ConnectionState.done) {  
                return const Text('Received data');  
            } else if (snapshot.connectionState == ConnectionState.waiting) {  
                return const CircularProgressIndicator.adaptive();  
            }  
            return Container();  
        });  
    }  
}

Now, we want to load these expensive widgets only when the parent List page is shown to a user, not when it is built. Yes, one way to solve this problem is with listeners, but they have to be passed from the top most widget all the way down to the expensive list view items. And the more nested the list view items are, the more problematic and uglier the code is.

Enter the InheritedWidget. They are immutable and they are never rendered. But same as any other Widget, they can be placed anywhere in the Widget Tree. So we'll create an InheritedWidget which will tell if the parent widget is visible to a user or not. We'll see later how those expensive list view items will get updates from it.


class IsVisible extends InheritedWidget {  
  final bool visible;  

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

  @override  
  bool updateShouldNotify(covariant InheritedWidget oldWidget) {  
      return oldWidget is IsVisible && oldWidget.visible != visible;  
  }  
}

updateShouldNotify method is the key here, in it we are telling Flutter to notify all Widgets that are depending on it when visibility changes. Now let's insert it inside our HomePage.


@override  
Widget build(BuildContext context) {  
  return Scaffold(  
  appBar: AppBar(title: Text(widget.title),),  
    body: AnimatedBuilder(animation: animationController, builder: (ctx, child) {  
      return Stack(children: childViews.map((v) {  
          int navIndex = childViews.indexOf(v);  
          bool isCurrent = navIndex == childIndex;  
          return IsVisible(isCurrent, // <- IsVisible is now the parent Widget to all child views in the Stack
                  child: TickerMode(enabled: isCurrent, // disable animations on all widgets except the topmost one              

Only thing that's left is to update our expensive list item to listen to our InheritedWidget. We do that by calling dependOnInheritedWidgetOfExactType on the BuildContext. It returns a nullable type, because null will be returned if Flutter can not find any widget of this type in the parent widget tree.


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

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

class _ExpensiveListItemState extends State {  
    bool isVisible = false;  

    @override  
    void didChangeDependencies() {  
        IsVisible? inheritedWidget = context.dependOnInheritedWidgetOfExactType();  
        isVisible = inheritedWidget?.visible ?? false;  
        super.didChangeDependencies();  
    }  

    @override  
    Widget build(BuildContext context) {  
        return isVisible  
  ? FutureBuilder(  
                future: Future.delayed(const Duration(seconds: 3)),  
        builder: (c, snapshot) {  
            if (snapshot.connectionState == ConnectionState.done) {  
                return const Text('Received data');  
            } else if (snapshot.connectionState == ConnectionState.waiting) {  
                return const CircularProgressIndicator.adaptive();  
            }  
            return Container();  
        })  
        : Container();  
    }  
}

And that is it. If you run the app now, the List page get's built immediately, but the bottom most list view items don't load until a user switches to this page. No listeners used, thanks to InheritedWidget.

The full code example is here