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