In Flutter, you’ve likely come across third-party state management packages like Provider or Bloc. You’ve undoubtedly interacted with core utilities like Theme
, Navigator
, or even MediaQuery
. All these tools have something in common: They’re powered by InheritedWidget, a foundational widget that propagates state down the widget tree. In this tutorial, you’ll harness the same power to complete the Weather++ app. And by the end, you’ll be able to answer the following questions:
- What is InheritedWidget and how does it work?
- How do you use InheritedWidget?
Getting Started
Download the project by clicking the Download materials button at the top or bottom of this tutorial. Unzip the project, and you’ll find two folders: starter and final. The final directory is the completed project, and the starter directory is the starter project where you’ll work from now on. Open the starter project with the latest version of Android Studio or Visual Studio Code, and you’ll find a similar project structure:
The folders outlined in red are specific to this project, while the others are Flutter boilerplates.
- assets/secrets.json: JSON file for storing keys like the API key; you’ll use this key to fetch the weather data in later steps.
- lib/location: Contains Dart files for handling user location. You’ll create more files in this directory later.
- lib/weather: Deals with weather-specific Dart files like widgets and data classes.
- lib/constants.dart: Container for app-wide constants.
- lib/home.dart: Contains widgets you see the first time you launch the app.
-
lib/main.dart: Initializes the app and wraps the
HomeWidget
in aMaterialApp
. - lib/secrets.dart: Houses the logic that loads the secrets in assets/secrets.json.
Now, open pubspec.yaml and click the Pub get tab that appears in your IDE. Run the project to see this on your target emulator or device:
This is the basic structure of the app. The white boxes are widget placeholders; you’ll replace them with real widgets as you progress with this tutorial.
Overview of Inherited Widgets
You’ll start this section by delving into the concept of InheritedWidget, understanding its definition, significance, and functionality. Later, you’ll explore how it differs from StatefulWidget
and StatelessWidget
.
What Is an InheritedWidget?
InheritedWidget is a foundational widget in the Flutter framework that enables efficient propagation of state down the widget tree. It uses the build context to share state and rebuilds dependent widgets when this state changes. Without InheritedWidget, if you were to share ThemeData
— like light and dark mode — between a parent and a child widget, it would look like this:
class ParentWidget extends StatelessWidget {
final ThemeData theme;
const ParentWidget({Key? key, required this.theme}) : super(key: key);
@override
Widget build(BuildContext context) {
return ChildWidget(
theme: theme,
);
}
}
class ChildWidget extends StatelessWidget {
final ThemeData theme;
const ChildWidget({Key? key, required this.theme}) : super(key: key);
@override
Widget build(BuildContext context) {
return IconButton(
onPressed: () {
// TODO: Implement functionality to change the app theme.
},
icon: Icon(theme.brightness == Brightness.dark
? Icons.nightlight
: Icons.sunny));
}
}
With this approach, ThemeData
would need to be passed manually to every widget that needs to access it. Not only does this introduce tight coupling, but widgets also won’t be rebuilt automatically when the theme changes, leading to potential inconsistencies in the UI. Utilizing the power of InheritedWidget, this is how child widgets will access the latest ThemeData
:
class ChildWidget extends StatelessWidget {
const ChildWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return IconButton(
onPressed: () {
// TODO: Implement functionality to change the app theme.
},
icon: Icon(Theme.of(context).brightness == Brightness.dark
? Icons.nightlight
: Icons.sunny));
}
}
You don’t need to pass the ThemeData
to this widget, and when the theme changes, it’ll automatically be rebuilt.
Differences Between StatelessWidget, StatefulWidget, and InheritedWidget
A StatelessWidget
is immutable, meaning once you set its properties, it cannot be changed. It’s useful for static content that doesn’t need to be mutated by the widget itself. On the other hand, a StatefulWidget
can be mutable, as it maintains a separate state object(s) that can be altered over the widget’s lifecycle. This makes it suitable for dynamic content that might change over time. In contrast, an InheritedWidget
is a special kind of widget designed to efficiently propagate information down the widget tree. Instead of passing data manually to each widget, descendants of an InheritedWidget
can access the data it holds directly, making it a powerful tool for state propagation and management.
State Propagation and State Mananagment
To exemplify and understand InheritedWidget
, you’ll work on the Weather++ app, the project you downloaded in the preceding steps. Using InheritedWidget
, you’ll build a location picker whose selection state will be readable and writable from any widget in the build tree. The app uses the selected location to fetch and display the current and future weather forecasts.
State Propagation with InheritedLocation
The first step to using an InheritedWidget
is to subclass it. So, in the starter project, create the file inherited_location.dart inside the location package, and add the code below:
import 'package:flutter/widgets.dart';
import 'location_data.dart';
class InheritedLocation extends InheritedWidget {
final LocationData? location;
const InheritedLocation(
{Key? key, required Widget child, required this.location})
: super(key: key, child: child);
}
InheritedLocation
is an InheritedWidget
, and it’ll rebuild all dependent widgets when location
changes. LocationData
is a data container that holds the latitude, longitude, and name of a specific location. The first two properties are used to fetch the weather data from OpenWeather, a widely used service for accessing real-time weather data. The name, on the other hand, will be displayed in the location picker widget.
But how will InheritedLocation
know when to rebuild? This is where updateShouldNotify()
comes in. Override it below the constructor as shown below:
@override
bool updateShouldNotify(InheritedLocation oldWidget) {
return oldWidget.location != location ||
oldWidget.location?.name.isEmpty == true;
}
It will rebuild dependent widgets when location
changes or if its name is empty. More on the empty condition later.
So how does InheritedLocation
know its dependencies? context.dependOnInheritedWidgetOfExactType
is the answer. Calling it using the context
of the dependent widget does two things. First, the framework registers the calling widget as a dependent of InheritedLocation
. Secondly, it returns the reference to the instance of InheritedLocation
.
For brevity, wrap that code in a little helper function called of()
in the InheritedLocation
class, below updateShouldNotify()
:
static InheritedLocation of(BuildContext context) {
final result =
context.dependOnInheritedWidgetOfExactType();
assert(result != null, 'No InheritedLocation found in context');
return result!;
}
This way, you’ll use InheritedLocation.of(context)
like Theme
, Provider
, and other widgets powered by InheritedWidget
.